Avoid Reloading Application Context on Unit Tests
A Zone Leader provides insight on how to cut down on unit test execution time, when mocking related services as part of a unit test.
Join the DZone community and get the full member experience.
Join For Free
Glancing through my articles on DZone, one can easily conclude that I am a fan of the Spring Boot framework for RESTful application development. That same query could easily conclude that I am a fan of providing ample unit test coverage for those very same RESTful APIs.
On a recent project, I realized that the use of @MockBean in my unit tests was having a major impact on the execution time for the test phase within our CI/CD pipeline.
This article talks about one way to resolve this issue.
You may also like: Unit Testing: The Good, Bad, and Ugly
@MockBean
The @MockBean annotation will add mock objects into the Spring application context. As a result, the mocked item will replace any existing bean of the same type in the application context. When this happens, Spring Boot realizes the application context has to be reloaded as part of the test initialization.
While this may not seem like a big deal, especially when this is typically a 10-15 second process, it can have an impact on the time to deploy — when there is a large code base to cover.
A Simple Example
Consider this simple service class example.
x
public class WidgetServiceImpl implements WidgetService {
private final AccountService accountService;
private final WidgetRepository widgetRepository;
/**
* {@inheritDoc}
*/
public List<Widget> getWidgetsByAccountId(Long accountId, String authId) throws AccountException {
Account account = accountService.getAccountById(accountId, authId);
return widgetRepository.getWidgetsByAccountId(accountId);
}
}
In the example above, constructor-based injections are being used, via Lombok's @RequiredArgsConstructor annotation, which is why a public constructor does not exist within the code.
For the getWidgetsByAccountId method to succeed, the accountId and authId must be provided to the AccountService in order to return a valid Account. This step is merely to help illustrate my point in the unit tests — when a service is dependent on something else. Basically, the business rule states that the WidgetService needs to interact with the AccountService as part of the request process.
If the AccountService does not throw an AccountException, the WidgetRepository (JPA interface) will simply return a List<Widget> for the provided accountId.
Unit Testing Background
In this example, both the AccountService and WidgetRepository needed to be mocked in order to provide information for the WidgetService...or the system under test (SUT).
I have always been a fan of keeping unit tests focused on the SUT and favor use of mocking over utilizing a database connection — which may or may not always have the data needed for the unit test to work properly.
In the WidgetServiceTest class, the Mockito when() method is utilized to interface with the mocked services and provide simulated results, which are geared to test the SUT.
Some examples may include:
- Providing the expected Account data to process the results.
- Providing the expected Widget repository data to process the results.
- Throwing exceptions to validate the AccountException fires.
By mocking the injected dependencies, the unit tests remain focused on the SUT.
Testing the Simple Example Using @MockBean
With Spring Boot, it is very possible to configure the WidgetServiceTest using @MockBean and @Autowired:
xxxxxxxxxx
public class WidgetServiceTest extends BaseServiceTest {
AccountService accountService;
WidgetRepository widgetRespository;
WidgetServiceImpl widgetService;
...
}
Every service which uses @MockBean for the AccountService will cause the Application Context to be reloaded within Sprint Boot. In my case, this added a 12 second delay before the first test in the class could be executed.
Updating the Simple Example
In the example below, the same service class can be tested without using @MockBean and causing the Application Context to be reloaded:
xxxxxxxxxx
public class WidgetServiceTest extends BaseServiceTest {
private final AccountService accountService = Mockito.mock(AccountService.class);
private final WidgetRepository widgetRespository = Mockito.mock(WidgetRepository.class);
WidgetServiceImpl widgetService = new WidgetService(accountService, widgetRepository);
...
}
Conclusion
The impact on making this change, ended up resolving issues with the build process and the wait time feature teams had to endure when updates became available for the RESTful API. This led to a faster turnaround time, which had positive impacts on the project's productivity.
There are certainly cases where @MockBean can be employed and not have a negative impact on the unit test process. In the scenario where there are dependencies on other services — which are likely employed by other services — Spring Boot will require the Application Context to be reloaded before the tests can begin. In that case, expect some additional time.
Have a great day!
Further Reading
Best Practices for Writing Unit Tests
Opinions expressed by DZone contributors are their own.
Comments