In my last article, we talked about how to efficiently build and improve these tests using Parasoft Jtest’s Unit Test Assistant. In this article, I’ll continue to discuss one of the biggest challenges of testing any complex application: dependency management.
Why do I need to be Mocking?
To tell the truth. Complex applications are not built from scratch — they use libraries, apis, and core projects or services that others have built and maintained. As Spring developers, we take advantage of what we already have so that we can spend our time and energy on what we care about: the business logic of our applications. We leave the details to the library, so our application has many dependencies, as shown in orange below.
Figure 1. A Spring service with multiple dependencies
So how do I focus unit tests on my application (controllers and services) if most of the functionality of my application depends on these dependent behaviors? Finally, am I always performing integration tests instead of unit tests? What if I need better control over the behavior of these dependencies, or if the dependencies become unavailable during unit testing?
What I need is a way to isolate my application from these dependencies so THAT I can focus unit testing on my application code. In some cases, we can create special “test” versions for these dependencies. However, using a standardized library like Mockito has several advantages over this approach.
- You don’t need to write and maintain special “test” code yourself.
- Emulation libraries can track calls to emulation, providing an additional layer of validation.
- Standard libraries like PowerMock provide additional functionality, such as emulating static methods, private methods, or constructors.
- Knowledge of mock libraries like Mockito can be reused across projects, while knowledge of custom test code cannot.
Figure 2. A mock service replaces multiple dependencies.
Spring dependencies
In general, Spring applications split functionality into beans. A Controller may depend on a Service Bean, which may depend on an EntityManager, JDBC connection, or another Bean. Most of the time, the dependencies that need to be isolated from the code under test are beans. In integration testing, all layers should be real — but for unit testing, we need to decide which dependencies should be real and which should be mock.
Spring allows developers to define and configure beans using XML, Java, or a combination of both to provide a mix of mock and real beans in your configuration. Since mock objects need to be defined in Java, you should use a Configuration class to define and configure mocked Beans.
Simulation depends on
When UTA generates a Spring test, all of your controller’s dependencies are set up to mock so that each test gains control of the dependencies. When the test runs, UTA detects method calls made on mock objects to methods that have not yet been configured for method emulation and suggests that these methods should be emulated. We can then use quick fixes to simulate each method automatically.
Here is an example controller that depends on PersonService:
@Controller
@RequestMapping("/people")
public class PeopleController {
@Autowired
protected PersonService personService;
@GetMapping
public ModelAndView people(Model model){
for (Person person : personService.getAllPeople()) {
model.addAttribute(person.getName(), person.getAge());
}
return new ModelAndView("people.jsp", model.asMap());
}
}
Copy the code
Here’s another example test, generated by Parasoft Jtest’s unit test assistant:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class PeopleControllerTest { @Autowired PersonService personService; // Other fields and setup @Configuration static class Config { // Other beans @Bean public PersonService getPersonService() { return mock(PersonService.class); } } @Test public void testPeople() throws Exception { // When ResultActions actions = mockMvc.perform(get("/people")); }}Copy the code
In this case, the test uses an inner class annotated with @Configuration that provides bean dependencies for the Controller under test using Java Configuration. This allows us to emulate the PersonService in the bean method. No methods have been simulated yet, so when I run the tests, I see the following suggestions:
This means that the getAllPeople() method is called on the PersonService I mock, but the test has not configured the mock for this method. When I select the “Mock It “quick fix option, the test is updated:
@Test
public void testPeople() throws Exception {
Collection<Person> getAllPeopleResult = new ArrayList<Person>();
doReturn(getAllPeopleResult).when(personService).getAllPeople();
// When
ResultActions actions = mockMvc.perform(get("/people"));
Copy the code
When I ran the test again, it passed. I should still populate the Collection returned by getAllPeople(), but the challenge of setting up my mock dependencies has been solved.
Note that I can move the generated method emulation from the test method to the bean method of the configuration class. If I do this, it means that every test in the class emulates the same method in the same way. Keeping method emulation in the test method means that the method can be simulated differently between different tests.
Spring Boot
Spring Boot makes bean mocking even simpler. You don’t have to use the @AutoWired field for the bean in test, nor the Configuration class that defines it. You just use a field for the bean and annotate it with @MockBean. Spring Boot will use a mocking framework it finds on the classpath to create a mock for a bean and inject it just like any other bean in the container. When generating Spring Boot tests using the unit Test Assistant, the @MockBean functionality is used instead of the Configuration class.
@springbooTtest @AutoConfiguRemockMVC Public class PeopleControllerTest {// Other fields and setup -- no Configuration class needed! @MockBean PersonService personService; @Test public void testPeople() throws Exception { ... }}Copy the code
XML versus Java configuration
In the first example above, the Configuration class provides all the beans to the Spring container. Alternatively, you can use XML Configuration instead of the Configuration class for testing; Or you can combine the two. For example, you can use:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({ "classpath:/**/testContext.xml" })
public class PeopleControllerTest {
@Autowired
PersonService personService;
// Other fields and setup
@Configuration
static class Config {
@Bean
@Primary
public PersonService getPersonService() {
return mock(PersonService.class);
}
}
// Tests
}
Copy the code
In this case, the class references an XML configuration file in the @ContextConfiguration annotation (not shown here) to provide most of the beans, which can be real beans or test-specific beans. We also provide an @Configuration class where PersonService is emulated. The @Primary annotation indicates that even if the PersonService bean is found in the XML Configuration, the test will use the mock bean from the @Configuration class instead. This type of configuration makes the test code smaller and easier to manage.
You can configure UTA to generate tests using any specific @ContextConfiguration property you need.
Analog static method
Sometimes, dependencies are accessed statically. For example, an application might access a third-party service through a static method call.
public class ExternalPersonService { public static Person getPerson(int id) { RestTemplate restTemplate = new RestTemplate(); try { return restTemplate.getForObject("http://domain.com/people/" + id, Person.class); } catch (RestClientException e) { return null; }}}Copy the code
In our controller:
@GetMapping public ResponseEntity<Person> getPerson(@PathVariable("id") int id, Model model) { Person person = ExternalPersonService.getPerson(id); if (person ! = null) { return new ResponseEntity<Person>(person, HttpStatus.OK); } return new ResponseEntity<>(HttpStatus.NOT_FOUND); }Copy the code
In this example, our processing method uses a static method call to get the Person object from a third-party service. When we build a JUnit test for this processing method, each test run time to real HTTP invocation of services, rather than a static simulation ExternalPersonService. GetPerson () method.
Instead, let us simulate the static ExternalPersonService. GetPerson () method. This avoids HTTP calls and allows us to provide a Person object response that fits our testing requirements. The Unit Test assistant makes it easier to simulate static methods with PowerMockito.
UTA generates a test for the above handler method that looks like this:
@Test
public void testGetPerson() throws Throwable {
// When
long id = 1L;
ResultActions actions = mockMvc.perform(get("/people/" + id));
// Then
actions.andExpect(status().isOk());
}
Copy the code
When we run the test, we will see the HTTP call in progress in the UTA flow tree. Let us find the ExternalPersonService. GetPerson () call, and carries on the simulation:
The test has been updated to test static methods using PowerMock:
@Test
public void testGetPerson() throws Throwable {
spy(ExternalPersonService.class);
Person getPersonResult = null; // UTA: default value
doReturn(getPersonResult).when(ExternalPersonService.class, "getPerson", anyInt());
// When
int id = 0;
ResultActions actions = mockMvc.perform(get("/people/" + id));
// Then
actions.andExpect(status().isOk());
}
Copy the code
Using UTA, we can now select the getPersonResult variable and instantiate it so that the simulated method call does not return NULL:
String name = ""; // UTA: default value
int age = 0; // UTA: default value
Person getPersonResult = new Person(name, age);
Copy the code
When we run the test again, getPersonResult from mockedExternalPersonService. GetPerson () method returns, the test pass.
Note: From the process tree, you can also select “Add Mock Method pattern” to make static method calls. This will configure the Unit Test Assistant to always simulate these static method calls when generating new tests.
conclusion
Complex applications often have functional dependencies that complicate and limit a developer’s ability to unit test the code. Using a simulation framework like Mockito helps developers isolate the code under test from these dependencies, enabling them to write better unit tests more quickly. The Parasoft Jtest unit testing assistant makes dependency management easy by configuring new tests to use mocks, as well as finding missing method mocks at run time and helping developers generate mocks for them.