|
| 1 | +# Automated test generation for Spring-based code |
| 2 | + |
| 3 | +Java developers actively use the Spring framework to implement the inversion of control and dependency injection. |
| 4 | +Testing Spring-based applications differs significantly from testing standard Java programs. Thus, we customized |
| 5 | +UnitTestBot to analyze Spring projects. |
| 6 | + |
| 7 | +<!-- TOC --> |
| 8 | + * [General notes](#general-notes) |
| 9 | + * [Limitations](#limitations) |
| 10 | + * [Testability](#testability) |
| 11 | + * [Standard unit tests](#standard-unit-tests) |
| 12 | + * [Example](#example) |
| 13 | + * [Use cases](#use-cases) |
| 14 | + * [Spring-specific unit tests](#spring-specific-unit-tests) |
| 15 | + * [Example](#example-1) |
| 16 | + * [Use cases](#use-cases-1) |
| 17 | + * [Side effects](#side-effects) |
| 18 | + * [Mechanism](#mechanism) |
| 19 | + * [Integration tests](#integration-tests) |
| 20 | + * [Service layer](#service-layer) |
| 21 | + * [Side effects](#side-effects-1) |
| 22 | + * [Use cases](#use-cases-2) |
| 23 | + * [Controller layer](#controller-layer) |
| 24 | + * [Example](#example-2) |
| 25 | + * [Microservice layer](#microservice-layer) |
| 26 | +<!-- TOC --> |
| 27 | + |
| 28 | +## General notes |
| 29 | + |
| 30 | +UnitTestBot proposes three approaches to automated test generation: |
| 31 | +* [standard unit tests](#standard-unit-tests) that mock environmental interactions; |
| 32 | +* [Spring-specific unit tests](#spring-specific-unit-tests) that use information about the Spring application context to reduce the number of |
| 33 | + mocks; |
| 34 | +* and [integration tests](#integration-tests) that validate interactions between application components. |
| 35 | + |
| 36 | +Hereinafter, by _components_ we mean Spring components. |
| 37 | + |
| 38 | +For classes under test, one should select an appropriate type of test generation based on their knowledge |
| 39 | +about the Spring specifics of the current class. Recommendations on how to choose the test type are provided below. |
| 40 | +For developers who are new to Spring, there is a "default" generation type. |
| 41 | + |
| 42 | +### Limitations |
| 43 | + |
| 44 | +UnitTestBot Java with Spring support uses symbolic execution to generate unit tests, so typical problems |
| 45 | +related to this technique may appear: it may be not so efficient for multithreaded programs, functions with calls to |
| 46 | +external libraries, processing large collections, etc. |
| 47 | + |
| 48 | +### Testability |
| 49 | + |
| 50 | +Note that UnitTestBot may generate unit tests more efficiently if your code is written to be unit-testable: the |
| 51 | +functions are not too complex, each function implements one logical unit, static and global data are used |
| 52 | +only if required, etc. Difficulties with automated test generation may have "diagnostic" value: it |
| 53 | +may mean that you should refactor your code. |
| 54 | + |
| 55 | +## Standard unit tests |
| 56 | + |
| 57 | +The easiest way to test Spring applications is to generate unit tests for components: to |
| 58 | +mock the external calls found in the method under test and to test just this method's |
| 59 | +functionality. UnitTestBot Java uses the Mockito framework that allows to mark |
| 60 | +the to-be-mocked objects with the `@Mock` annotation and to use the `@InjectMock` |
| 61 | +annotation for the tested instance injecting all the mocked fields. See [Mockito](https://site.mockito.org/) |
| 62 | +documentation for details. |
| 63 | + |
| 64 | +### Example |
| 65 | + |
| 66 | +Consider generating unit tests for the `OrderService` class that autowires `OrderRepository`: |
| 67 | + |
| 68 | +```java |
| 69 | + |
| 70 | +@Service |
| 71 | +public class OrderService { |
| 72 | + |
| 73 | +@Autowired |
| 74 | +private OrderRepository orderRepository ; |
| 75 | + |
| 76 | +public List<Order> getOrders () { |
| 77 | +return orderRepository.findAll (); |
| 78 | +} |
| 79 | +} |
| 80 | + |
| 81 | +public interface OrderRepository extends JpaRepository <Order, Long> |
| 82 | +``` |
| 83 | +Then we mock the repository and inject the resulting mock into a service: |
| 84 | + |
| 85 | +```java |
| 86 | +public final class OrderServiceTest { |
| 87 | + @InjectMocks |
| 88 | + private OrderService orderService |
| 89 | + |
| 90 | + @Mock |
| 91 | + private OrderRepository orderRepositoryMock |
| 92 | + |
| 93 | + @Test |
| 94 | + public void testGetOrders () { |
| 95 | + when(orderRepositoryMock .findAll()).thenReturn((List)null) |
| 96 | + |
| 97 | + List actual = orderService .getOrders() |
| 98 | + assertNull(actual) |
| 99 | + } |
| 100 | +``` |
| 101 | + |
| 102 | +This test type does not process the Spring context of the original application. The components are tested in |
| 103 | +isolation. |
| 104 | + |
| 105 | +It is convenient when the component has its own meaningful logic and may be useless when its main responsibility is to call other components. |
| 106 | + |
| 107 | +Note that if you autowire several beans of one type or a collection into the class under test, the code of test |
| 108 | +class will be a bit different: for example, when a collection is autowired, it is marked with `@Spy` annotation due |
| 109 | +to Mockito specifics (not with `@Mock`). |
| 110 | + |
| 111 | +### Use cases |
| 112 | + |
| 113 | +When to generate standard unit tests: |
| 114 | +* _Service_ or _DAO_ layer of Spring application is tested. |
| 115 | +* Class having no Spring specific is tested. |
| 116 | +* You would like to test your code in isolation. |
| 117 | +* You would like to generate tests as fast as possible. |
| 118 | +* You would like to avoid starting application context and be sure the test generation process has no Spring-related side effects. |
| 119 | +* You would like to generate tests in one click and avoid creating specific profiles or configuration classes for |
| 120 | + testing purposes. |
| 121 | + |
| 122 | +We suggest using this test generation type for the users that are not so experienced in Spring or would like to get |
| 123 | +test coverage for their projects without additional efforts. |
| 124 | + |
| 125 | +## Spring-specific unit tests |
| 126 | + |
| 127 | +This is a modification of standard unit tests generated for Spring projects that may allow us to get more |
| 128 | +meaningful tests. |
| 129 | + |
| 130 | +### Example |
| 131 | + |
| 132 | +Consider the following class under test |
| 133 | + |
| 134 | +```java |
| 135 | +@Service |
| 136 | +public class GenderService { |
| 137 | + |
| 138 | +@Autowired |
| 139 | +public Human human |
| 140 | + |
| 141 | +public String getGender () { |
| 142 | +return human.getGender(); |
| 143 | +} |
| 144 | +} |
| 145 | +``` |
| 146 | +where `Human` is an interface that has just one implementation actually used in current project configuration. |
| 147 | + |
| 148 | +```java |
| 149 | +public interface Human { |
| 150 | +String getGender(); |
| 151 | +} |
| 152 | + |
| 153 | +public class Man implements Human { |
| 154 | +public String getGender() { |
| 155 | +return “man” |
| 156 | +} |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +The standard unit test generation approach is to mock the _autowired_ objects. It means that the generated test will be |
| 161 | +correct but useless. However, there is just one implementation of the `Human` interface, so we may use it directly |
| 162 | +and generate a test like this: |
| 163 | + |
| 164 | +```java |
| 165 | +@Test |
| 166 | +public void testGetGender_HumanGetGender() { |
| 167 | +GenderService genderService = new GenderService(); |
| 168 | +genderService.human = new Man(); |
| 169 | +String actual = genderService.getGender(); |
| 170 | +assertEquals(“man”, actual); |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +Actually, dependencies in Spring applications are often injected via interfaces, and they often have just one actual |
| 175 | +implementation, so it can be used in the generated tests instead of an interface. If a class is injected itself, it |
| 176 | +will also be used in tests instead of a mock. |
| 177 | + |
| 178 | +You need to select a configuration to guide the process of creating unit tests. We support all commonly used |
| 179 | +approaches to configure the application: |
| 180 | +* using an XML file, |
| 181 | +* Java annotation, |
| 182 | +* or automated configuration in Spring Boot. |
| 183 | + |
| 184 | +Although it is possible to use the development configuration for testing purposes, we strictly recommend creating a separate one. |
| 185 | + |
| 186 | +### Use cases |
| 187 | + |
| 188 | +When to generate Spring-specific unit tests: |
| 189 | +* to reduce the amount of mocks in generated tests |
| 190 | +* and to use real object types instead of their interfaces, obtaining tests that simulate the method under test execution. |
| 191 | + |
| 192 | +### Side effects |
| 193 | + |
| 194 | +We do not recommend generating Spring-specific unit tests, when you would like to maximize line coverage. |
| 195 | +The goal of this approach is to cover the lines that are relevant for the current configuration and are to be used |
| 196 | +during the application run. The other lines are ignored. |
| 197 | + |
| 198 | +When a concrete object is created instead of mocks, it is analyzed with symbolic execution. It means that the |
| 199 | +generation process may take longer and may exceed the requested timeout. |
| 200 | + |
| 201 | +### Mechanism |
| 202 | + |
| 203 | +A Spring application is created to simulate a user one. It uses configuration importing users one with an additional |
| 204 | +bean of a special _bean factory post processor_. |
| 205 | + |
| 206 | +This _post processor_ is called when bean definitions have already been created, but actual bean initialization has |
| 207 | +not been started. It gets all accessible information about bean types from the definitions and destroys these |
| 208 | +definitions after that. |
| 209 | + |
| 210 | +Further Spring context initialization is gracefully crashed as bean definitions do not exist anymore. Thus, this |
| 211 | +test generation type is still safe and will not have any Spring-related side effects. |
| 212 | + |
| 213 | +Bean type information is used in symbolic execution to decide if we should mock the current object or instantiate it. |
| 214 | + |
| 215 | +## Integration tests |
| 216 | + |
| 217 | +The main difference of integration testing is that it tests the current component while taking interactions with |
| 218 | +other classes into account. |
| 219 | + |
| 220 | +### _Service_ layer |
| 221 | + |
| 222 | +Consider an `OrderService` class we have already seen. Actually, this class has just one |
| 223 | +responsibility: to return the result of a call to the repository. So, if we mock the repository, our unit test is |
| 224 | +actually useless. However, we can test this service in interaction with the repository: save some information to the |
| 225 | +database and verify if we have successfully read it in our method. Thus, the test method looks as follows. |
| 226 | + |
| 227 | +```java |
| 228 | + |
| 229 | +@Autowired |
| 230 | +private OrderService orderService |
| 231 | + |
| 232 | +@Autowired |
| 233 | +private OrderRepository orderRepository |
| 234 | + |
| 235 | +@Test |
| 236 | +public void testGetOrderById() throws Exception { |
| 237 | +Order order = new Order(); |
| 238 | +Order order1 = orderRepository.save(order); |
| 239 | +long id = (Long) getFieldValue(order1, "com.rest.order.models.Order ", "id“); |
| 240 | +
|
| 241 | +Order actual = orderService.getOrderById(id); |
| 242 | +assertEquals (order1, actual); |
| 243 | +} |
| 244 | +``` |
| 245 | +The key idea of integration testing is to initialize the context of a Spring application and to autowire a bean of |
| 246 | +the class under test, and the beans it depends on. The main difficulty is to mutate the initial _autowired_ state of the |
| 247 | +object under test to another state to obtain meaningful tests (e.g. save some data to related repositories). |
| 248 | +Here we use fuzzing methods instead of symbolic execution. |
| 249 | +
|
| 250 | +You should take into account that our integration tests do not use mocks at all. It also means that if the method |
| 251 | +under test contains calls to other microservices, you need to start the microservice unless you want to test your |
| 252 | +component under an assumption that the microservice is not responding. |
| 253 | +Writing tests manually, users can investigate the expected behavior of the external service for the current scenario, |
| 254 | +but automated test generation tools have no way to do it. |
| 255 | +
|
| 256 | +Note that XML configuration files are currently not supported in integration testing. However, you may create a Java |
| 257 | +configuration class importing your XML file as a resource. The list of supported test |
| 258 | +frameworks is reduced to JUnit 4 and JUnit 5; TestNG is not supported for integration tests. |
| 259 | +
|
| 260 | +To run integration tests properly, several annotations are generated for the class with tests (some of them may be |
| 261 | +missed: for example, we can avoid setting active profiles via the annotation if a default profile is used). |
| 262 | +
|
| 263 | +* `@SpringBootTest` for Spring Boot applications |
| 264 | +* `@RunWith(SpringRunner.class)`/`@ExtendWith(SpringExtension.class)` depending on the test framework |
| 265 | +* `@BootstrapWith(SpringBootTestContextBootstrapper.class)` for Spring Boot applications |
| 266 | +* `@ActiveProfiles(profiles = {profile_names})` to activate requested profiles |
| 267 | +* `@ContextConfiguration(classes = {configuration_classes})` to initialize a proper configuration |
| 268 | +* `@AutoConfugureTestDatabase` |
| 269 | +
|
| 270 | +Two additional annotations are: |
| 271 | +
|
| 272 | +* `@Transactional`: using this annotation is not a good idea for some developers because it can |
| 273 | +hide problems in the tested code. For example, it leads to getting data from the transaction cache instead of real |
| 274 | +communication with database. |
| 275 | +However, we need to use this annotation during the test generation process due to the |
| 276 | +efficiency reasons and the current fuzzing approach. Generating tests in transaction but not running them in |
| 277 | +transaction may sometimes lead to failing tests. |
| 278 | +In future, we are going to modify the test generation process and to use `EntityManager` and manual flushing to the |
| 279 | +database, so running tests in transaction will not have a mentioned disadvantage any more. |
| 280 | +
|
| 281 | +* `@DirtiesContext(classMode=BEFORE_EACH_TEST_METHOD)`: although running test method in transaction rollbacks most |
| 282 | +actions in the context, there are two reasons to use `DirtiesContext`. First, we are going to remove |
| 283 | +`@Transactional`. After that, the database `id` sequences are not rolled back with the transaction, while we would |
| 284 | +like to have a clean context state for each new test to avoid unobvious dependencies between them. |
| 285 | +
|
| 286 | +Currently, we do not have proper support for Spring security issues in UnitTestBot. We are going to improve it in |
| 287 | +future releases, but to get at least some results on the classes requiring authorization, we use `@WithMockUser` for |
| 288 | +applications with security issues. |
| 289 | +
|
| 290 | +#### Side effects |
| 291 | +
|
| 292 | +Actually, yes! Integration test generation requires Spring context initialization that may contain unexpected |
| 293 | +actions: HTTP requests, calls to other microservices, changing the computer parameters. So you need to |
| 294 | +validate the configuration carefully before trying to generate integration tests. We strictly recommend avoiding |
| 295 | +using _production_ and _development_ configuration classes for testing purposes, and creating separate ones. |
| 296 | +
|
| 297 | +#### Use cases |
| 298 | +
|
| 299 | +When to generate integration tests: |
| 300 | +* You have a properly prepared configuration class for testing |
| 301 | +* You would like to test your component in interaction with others |
| 302 | +* You would like to generate tests without mocks |
| 303 | +* You would like to test a controller |
| 304 | +* You consent that generation may be much longer than for unit tests |
| 305 | +
|
| 306 | +### _Controller_ layer |
| 307 | +
|
| 308 | +When you write tests for controllers manually, it is recommended to do it a bit differently. Of course, you may just |
| 309 | +mock the other classes and generate unit tests looking similarly to the tests we created for services, but they may |
| 310 | +not be representative. To solve this problem, we suggest a specific integration test generation approach for controllers. |
| 311 | +
|
| 312 | +#### Example |
| 313 | +
|
| 314 | +Consider testing the following controller method: |
| 315 | +
|
| 316 | +```java |
| 317 | +
|
| 318 | +@RestController |
| 319 | +@RequestMapping(value = "/api") |
| 320 | +public class OrderController { |
| 321 | +
|
| 322 | + @Autowired |
| 323 | + private OrderService orderService; |
| 324 | +
|
| 325 | + @GetMapping(path = "/orders") |
| 326 | + public ResponseEntity<List<Order>> getAllOrders() { |
| 327 | + return ResponseEntity.ok().body(orderService.getOrders()); |
| 328 | + } |
| 329 | +} |
| 330 | +``` |
| 331 | +UnitTestBot generates the following integration test for it: |
| 332 | +
|
| 333 | +```java |
| 334 | +@Test |
| 335 | +public void testGetAllOrders() throws Exception { |
| 336 | +Object[] objectArray = {}; |
| 337 | +MockHttpServletRequestBuilder mockHttpServletRequestBuilder = get("/api/orders", objectArray); |
| 338 | +
|
| 339 | +ResultActions actual = mockMvc.perform(mockHttpServletRequestBuilder); |
| 340 | +
|
| 341 | +actual.andDo(print()); |
| 342 | +actual.andExpect((status()).is(200)); |
| 343 | +actual.andExpect((content()).string("[]")); |
| 344 | +} |
| 345 | +``` |
| 346 | +
|
| 347 | +Note that generating specific tests for controllers is now in active development, so some parameter annotations and |
| 348 | +types have not been supported yet. For example, we have not supported the `@RequestParam` annotation yet. For now, |
| 349 | +specific integration tests for controllers are just an experimental feature. |
| 350 | +
|
| 351 | +### _Microservice_ layer |
| 352 | +
|
| 353 | +Actually, during integration test generation we create one specific test that can be considered as a test for the |
| 354 | +whole microservice. It is the `contextLoads` test, and it checks if a Spring application context has started normally. |
| 355 | +If this test fails, it means that your application is not properly configured, so the failure of other tests is not caused by the regression in the tested code. |
| 356 | +
|
| 357 | +Normally, this test is very simple: |
| 358 | +
|
| 359 | +```java |
| 360 | +/** |
| 361 | +* This sanity check test fails if the application context cannot start. |
| 362 | + */ |
| 363 | + @Test |
| 364 | + public void contextLoads() { |
| 365 | + } |
| 366 | +``` |
| 367 | +
|
| 368 | +If there are context loading problems, the test contains a commented exception type, a message, and a |
| 369 | +track trace, so it is easier to investigate why context initialization has failed. |
| 370 | +
|
0 commit comments