BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles You’ve Completed Unit Testing; Your Testing has Just Begun

You’ve Completed Unit Testing; Your Testing has Just Begun

Lire ce contenu en français

Bookmarks

Nowadays, unit testing has become so widespread, developers who don’t practice it hold their heads down in shame. Wikipedia defines unit testing as:

A software testing method by which individual units of source code... are tested to determine whether they are fit for use.

In Object-Oriented languages, and particularly in Java, the general usage is that the “unit of source code” is the method. In order to illustrate this point, let’s use a simple example from Spring’s iconic Pet Clinic application. An excerpt of the PetController class is reproduced here for convenience:

@Controller
@SessionAttributes("pet")
public class PetController {

    private final ClinicService clinicService;

    @Autowired
    public PetController(ClinicService clinicService) {
        this.clinicService = clinicService;
    }

    @ModelAttribute("types")
    public Collection<PetType> populatePetTypes() {
        return this.clinicService.findPetTypes();
    }

    @RequestMapping(value = "/owners/{ownerId}/pets/new", method = RequestMethod.GET)
    public String initCreationForm(@PathVariable("ownerId") int ownerId,
                                   Map<String, Object> model) {
        Owner owner = this.clinicService.findOwnerById(ownerId);
        Pet pet = new Pet();
        owner.addPet(pet);
        model.put("pet", pet);
        return "pets/createOrUpdatePetForm";
    }
    ...
}

As we can see, the initCreationForm() method is meant to populate the HTML form to create new Pet instances. Our goal will be to test this method for:

  1. Putting a Pet instance in the model
  2. Setting the Owner of this Pet instance
  3. Returning a defined view

Plain-Old Classic Unit Testing

Unit testing - as described above - will require stubbing dependencies of the method. One typical unit test based on the popular Mockito framework would look like:

public class PetControllerTest {

    private ClinicService clinicService;
    private PetController controller;

    @Before
    public void setUp() {
        clinicService = Mockito.mock(ClinicService.class);
        controller = new PetController(clinicService);
    }

    @Test
    public void should_set_pet_in_model() {
        Owner dummyOwner = new Owner();
        Mockito.when(clinicService.findOwnerById(1)).thenReturn(dummyOwner);
        HashMap<String, Object> model = new HashMap<String, Object>();
        controller.initCreationForm(1, model);
        Iterator<Object> modelIterator = model.values().iterator();
        Assert.assertTrue(modelIterator.hasNext());
        Object value = modelIterator.next();
        Assert.assertTrue(value instanceof Pet);
        Pet pet = (Pet) value;
        Owner petOwner = pet.getOwner();
        Assert.assertNotNull(petOwner);
        Assert.assertSame(dummyOwner, petOwner);
    }
}
  • The setUp() method initializes the controller to test as well as the ClinicService dependency. As it is a dependency, Mockito helps to mock it.
  • The should_set_pet_in_model() checks that after the run of the method, the model contains a Pet instance and that it has for owner the one returned by the mocked ClinicService.
  • Note that the returning of the view is not tested, as it would exactly mirror the controller’s code.

What’s missing in Unit Testing

At this point, we have safely reached our 100% code coverage for the method and we could stop at this point. However, stopping just after unit testing the code is akin to starting mass production of automobiles after testing each nut and bolt of a car. Of course nobody would ever take such a huge risk; in real life, the car would first be taken on many test drives to check that the assembly of not just every nut and bolt, but every other part perform in coordinated orchestration as intended. In the software development world, test driving translates into what we affectionately refer to as integration testing. Integration testing guarantees that the collaboration of classes works.

In the Java world, both the Spring framework and the Java EE platforms are containers that provide APIs over available services, for example JDBC for database access. Making sure that applications developed with Spring or Java EE work as expected, require them to be tested in-container to test interactions with the services offered by the container.

In the above test example, some things are not tested - and cannot be:

  • The Spring configuration i.e. the assembly of all classes together through autowiring
  • The populating of the PetType in the model i.e. the populatePetTypes() method
  • The URL mapping to the right controller and method i.e. the @RequestMapping annotation
  • The setting of the Pet instance in the HTTP session i.e. the @SessionAttributes("pet")

In-Container Testing

Integration testing and its specialized flavor “in-container testing” is the solution to test the points above. Fortunately, Spring provides a whole test framework aimed at that, and with the Arquillian test framework, Java EE users are happily included. However Java EE applications have different assembly methods such as CDI, and Arquillian provides ways to cope with such differences. With that background, let’s get back to our Pet Clinic, and create tests to verify the points above.

Spring JUnit integration

As its name implies, JUnit is a framework for unit testing. Spring provides a dedicated JUnit runner to starts the Spring container at the start of the test. That is configured with the @RunWith annotation on the test class.

@RunWith(SpringJUnit4ClassRunner.class)
public class PetControllerIT { ... }

The Spring framework has its own set of configuration components; either legacy XML files or more recently Java “configuration” classes. A good practice is to have finer-grained configuration components, so that one can pick and choose just the ones that are necessary in the context of a test. Those configuration components can be set with the @ContextConfiguration annotation on the test class.

@ContextConfiguration("classpath:spring/business-config.xml")
public class PetControllerIT { ... }

Finally, Spring allows for some configuration bits to be activated (or not) based on an application-wide flag called a profile. Using a profile is as easy as setting the @ActiveProfile on the test class.

@ActiveProfiles("jdbc")
public class PetControllerIT { ... }

These are enough to test standard Spring beans with JUnit; but for testing Spring MVC controllers more effort is required.

Spring web context for tests

For web applications, Spring creates a structured context in a parent-child relationship in a layered architecture. The child holds what is related to the web, such as controllers, formatters, resource bundles, etc. The parent contains the rest, such as services and repositories.. To emulate this relationship, annotate the test class with the @ContextHierarchy annotation and configure it to reference the necessary @ContextConfiguration annotations:

In the following tester snippet, the business-config.xml is the parent and mvc-core-config.xml the child:

@ContextHierarchy({
    @ContextConfiguration("classpath:spring/business-config.xml"),
    @ContextConfiguration("classpath:spring/mvc-core-config.xml")
})

One also needs to set the @WebAppConfiguration annotation to emulate a WebApplicationContext instead of a simpler ApplicationContext.

Testing Controllers

Once the web context has been set for tests as described above, it’s finally possible to test controllers. The entry point for this is the MockMvc class, it contains the following attributes:

  • A Request Builder to create the Fake request
  • A Request Matcher to check result of a controller’s method execution
  • A Result Handler to do arbitrary stuff on the result

MockMvc instances are made available through static methods of the MockMvcBuilders class. One is aimed at a dedicated set of controllers, the other for the whole application context. In the latter case, a WebApplicationContext instance is required as a parameter. This is quite easy: just autowire an attribute of this type in the test class.

@RunWith(SpringJUnit4ClassRunner.class)
public class PetControllerIT {

    @Autowired
    private WebApplicationContext context;
}

Then, execution of the configured MockMvc is done through the perform(RequestBuilder) method. In turn, RequestBuilder instances are made available through static methods of the MockMvcRequestBuilders class. Each static method is a way to execute a specific HTTP method.

To sum it up, here’s how one can simulate a GET call to the /owners/1/pets/new path.

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(PetController.class).build();
RequestBuilder getNewPet = MockMvcRequestBuilders.get("/owners/1/pets/new");
mockMvc.perform(getNewPet);

Finally, Spring provides a lot of assertions through the ResultMatcher interface and the fluency of the MockMvc API: cookies, content, underlying model, HTTP status code, all of these can be checked and more.

Putting it all together

Everything is now available to test that earlier controller that couldn’t be tested with only unit testing.

1 @RunWith(SpringJUnit4ClassRunner.class)
 2 @ContextHierarchy({
 3    @ContextConfiguration("classpath:spring/business-config.xml"),
 4   @ContextConfiguration("classpath:spring/mvc-core-config.xml")
 5 })
 6 @WebAppConfiguration
 7 @ActiveProfiles("jdbc")
 8 public class PetControllerIT {
 9
10    @Autowired
11    private WebApplicationContext context;
12
13    @Test
14    public void should_set_pet_types_in_model() throws Exception {
15        MockMvc mockMvc = webAppContextSetup(context).build();
16        RequestBuilder getNewPet = get("/owners/1/pets/new");
17        mockMvc.perform(getNewPet)
18            .andExpect(model().attributeExists("types"))
19            .andExpect(request().sessionAttribute("pet", instanceOf(Pet.class)));
20    }
21 }

Here’s what’s going on In this snippet:

  • On lines 3 and 4, we guarantee the Spring configuration files are well-configured
  • On line 16, we ensure that the application responds to a GET call to the form preparation URL
  • On line 18, we test for the presence of the types attribute in the model
  • Finally, on line 19, we test for the presence of the pet attribute in the session, and verify it is of the expected Pet type

(Note the instanceOf() static method comes from the Hamcrest API.)

Other testing challenges

The previous PetClinic example was a somewhat simple one. It already uses a Hypersonic SQL database out-of-the-box, creates the database schema automatically and inserts some data, all of this when the container is started. In regular applications, one will probably use different databases for production and during tests. Also, the data won’t be initialized for production. Challenges will include how to switch to another database for testing, how to put the database in the required state before test execution and how to check the state after it.

Likewise, the PetController has been tested for the single initCreationForm() method, whereas the creation process also implies the processCreationForm() method. In order to reduce test initialization code, it is not unreasonable to test not each method but the use-case itself. This would probably imply a huge test method: if the test fails, it will be hard to locate the cause of the failure. Another approach would be to to create fine-grained correctly named methods and to execute them in order. Unfortunately JUnit, being a true unit testing framework,doesn’t allow for that.

Every component interacting with an infrastructure resource, such as database, mail server, FTP server, etc. faces the same challenge: mocking that resource adds no value for testing. For example, how can one check complex JPA queries? This requires more than just mocking the database; the common practice is to set up a dedicated in-memory database. There might be better alternatives, depending on the context. The challenge in this case is to choose the right alternative and to manage the lifecycle of the resource inside the test.

As infrastructure resources go, web services comprise a huge part of any modern web application’s dependencies. This is even getting worse regarding the current trend toward microservices. If one’s application is dependent on external web services, testing the collaboration of such application with its dependencies becomes a requirement. Of course, the setup of web service dependencies is hugely dependent on their nature, most commonly being either SOAP or REST.

Also, If the application is not targeted at Spring but at Java EE, challenges become different. Java EE provides the Context and Dependency Injection service, which is based on autowiring. Testing such an application means assembling the right set of components - classes and configuration files. Besides, Java EE promises that the same application can run on different compliant application servers. If the application has different target platforms, for example because it’s a product meant to be deployed to different customer environments, this compatibility has to be thoroughly tested.

Conclusion

In this article, I showed how techniques of integration testing can bring you more confidence in your code using the Spring MVC web framework as an example.

I also briefly showed how testing presents some challenges that cannot be solved by unit testing alone but require integration testing.

These are the basic techniques. For a deeper dive into other techniques and additional tools , please refer to the book “Integration Testing from the Trenches" by yours truly, where I present tools and techniques to use them to better guarantee the quality of your running software.

The book was reviewed on InfoQ and is available in all major electronic formats on Leanpub and in paperback on Amazon.

About the Author

Nicolas Fränkel operates as a successful Java and Java EE software architect and developer with more than 12 years experience in consulting for different clients. He also practices a trainer and a part-time lecturer in different French and Swiss higher education institutions, so as to broaden his understanding of software craftsmanship. Nicolas has also been a speaker at miscellaneous Java-related conferences in Europe, such as Deovxx Belgium, JEEConf, JavaLand and some Java User Groups and is the author of Learning Vaadin and Learning Vaadin 7. Nicolas's interests in software are broad, ranging from Rich Client Application, to open source software and build automation through Quality Processes, including diverse flavors of testing. 

Rate this Article

Adoption
Style

BT