In my past career, I often found that some people didn't write test code, and the reason they claimed not to write was that they could not easily write test cases covering multiple different modules. Well, I believe most of them either lack some more easy-to-master technical means or don’t have time to figure it out. After all, there will always be various pressures such as progress at work. Because I don’t know how to test, I often ignore integration testing, and the problems brought about are getting worse software, more and more bugs and more disappointed customers. So I want to share some personal experiences to unveil the mystery of integration testing.
How to better integrate testing of Spring-based projects <br />Using tools: Spring, JUnit, Mockito
Imagine there is such a Spring project that integrates some external services, such as some banking web services. Then, the problems encountered when writing test cases for this project and completing these tests in a continuous integration system are basically the same:
1. There will be transactions in each test, and each transaction requires a monetary cost, which will ultimately be borne by the customer;
2. Excessive requests issued during testing may be considered malicious requests, which may cause the account in the bank to be blocked, with the consequence of the test failure;
3. When testing is performed using a non-production environment, the test results are not very reliable. Similarly, the consequence is that the test fails.
Usually, when you test a single class, the problem is easy to solve because you can virtualize some external services for call. But when testing the entire huge business process, it means you need to test multiple components, and at this time, you need to incorporate these components into the Spring container for management. Fortunately, Spring includes a very excellent testing framework that allows you to inject beans from production environment configuration files into the test environment, but for those called external services, we need to write mock implementations ourselves. The first reaction of ordinary people may be to reinject (modify) the beans injected by Spring during the setUp stage of the test, but this method needs to be carefully considered.
Warning: In this way, your test code breaks the container's own behavior, so there is no guarantee that it will be the same as the results of your test in a real environment.
In fact, instead of implementing the mock class first and then reinjecting it into the required bean, we can let Spring help us inject the mock class from the beginning. Let's demonstrate it with code.
The sample project contains a class called BankService, representing the call to the external service, and a class called UserBalanceService, which calls BankService. UserBalanceService is implemented very simple, just to complete the conversion of balance from String to Double type.
Source code of BankService.java:
public interface BankService { String getBalanceByEmail(String email);} Source code of BankServiceImpl.java:
public class BankServiceImpl implements BankService { @Override public String getBalanceByEmail(String email) { throw new UnsupportedOperationException("Operation failed due to external exception"); }} Source code of UserBalanceService.java:
interface UserBalanceService { Double getAccountBalance(String email);} Source code of UserBalanceServiceImpl.java:
public class UserBalanceServiceImpl implements UserBalanceService { @Autowired private BankService bankService; @Override public Double getAccountBalance(String email) { return Double.valueOf(bankService.getBalanceByEmail(email)); }} Then there is Spring's XML configuration file, adding the required bean declaration.
Source code of applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="bankService"/> <bean id="userBalanceService"/></beans>
Here is the source code of the test class UserBalanceServiceImplTest.java:
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:/springtest/springockito/applicationContext.xml")public class UserBalanceServiceImplProfileTest { @Autowired private UserBalanceService userBalanceService; @Autowired private BankService bankService; @Test public void shouldReturnMockedBalance() { Double balance = userBalanceService.getAccountBalance("[email protected]"); assertEquals(balance, Double.valueOf(123.45D)); }} As we expected, the test method reports an UnsupportedOperationException exception. Our current purpose is to replace BankService with our simulation implementation. It is OK to use Mockito directly to generate factory beans, but there is a better option, use the Springockito framework. You can have a rough idea before continuing.
The remaining question is simple: how to inject Spring into a simulated bean instead of a real bean, there was no other way before Spring 3.1 except to create a new XML configuration file. But since Spring introduced the profile definition of beans, we have a more elegant solution, although this approach also requires an additional XML configuration file specifically for testing. Here is the code for the configuration file testApplicationContext.xml used to test:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mockito="http://www.mockito.org/spring/mockito" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.mockito.org/spring/mockito https://bitbucket.org/kubek2k/springockito/raw/tip/springockito/src/main/resources/spring/mockito.xsd"> <import resource="classpath:/springtest/springockito/applicationContext.xml"/> <beans profile="springTest"> <mockito:mock id="bankService"/> </beans></beans>
The source code of the test class UserBalanceServiceImplProfileTest.java after making corresponding modifications:
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:/springtest/springockito/testApplicationContext.xml")@ActiveProfiles(profiles = {"springTest"})public class UserBalanceServiceImplProfileTest { @Autowired private UserBalanceService userBalanceService; @Autowired private BankService bankService; @Before public void setUp() throws Exception { Mockito.when(bankService.getBalanceByEmail("[email protected]")).thenReturn(String.valueOf(123.45D)); } @Test public void shouldReturnMockedBalance() { Double balance = userBalanceService.getAccountBalance("[email protected]"); assertEquals(balance, Double.valueOf(123.45D)); }} You may have noticed that in the setUp method, we define the simulated behavior and add the @Profile annotation to the class. This annotation activates a profile called springTest, so the bean simulated using Springockito can be automatically injected anywhere it needs to. The run result of this test will be successful because Spring injects the version simulated by Springockito, not the version declared in applicationContext.xml.
Continue to optimize our tests
If we can further take the solution to this problem, this article will not seem to be flawed. Springockito offers another name called
Springockito Annotation framework, which allows us to use annotations in test classes to inject mock classes. Before you continue reading, it is better to go to the website to take a look. OK, here is the modified test code.
Source code of UserBalanceServiceImplAnnotationTest.java: @RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(loader = SpringockitoContextLoader.class, locations = "classpath:/springtest/springockito/applicationContext.xml")public class UserBalanceServiceImplAnnotationTest { @Autowired private UserBalanceService userBalanceService; @Autowired @ReplaceWithMock private BankService bankService; @Before public void setUp() throws Exception { Mockito.when(bankService.getBalanceByEmail("[email protected]")).thenReturn(String.valueOf(valueOf(123.45D))); } @Test public void shouldReturnMockedBalance() { Double balance = userBalanceService.getAccountBalance("[email protected]"); assertEquals(balance, valueOf(123.45D)); }} Please note that there is no newly introduced XML configuration file here, but the applicationContext.xml of the formal environment is directly used. We use the @ReplaceWithMock annotation to mark the bean of type BankService, and then define the behavior of the mock class in the setUp method.
postscript
The Springockito-annotations project has a huge advantage, that is, it builds our test code on dependency coverage, so that we do not need to define additional XML configuration files or modify the configuration files of the production environment for testing. If Springockito-annotations is not used, we have no choice but to define additional XML configuration files. Therefore, I strongly recommend that you use Springockito-annotations in your integration tests so that you can minimize the impact of your test cases on your production code, and also remove the burden of maintaining additional XML configuration files.
Postscript
Writing integration tests for Spring projects is really much easier. The code in the article refers to my own GitHub.
Translation link: http://www.codeceo.com/article/spring-test-is-easy.html
Original English: Test Me If You Can #1 (Spring Framework)
Translator: Sandbox Wang, Coding Network
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.