Integration testing with the Spring Test context framework

When individual program units are combined and tested as a group, then it is known as integration testing. The Spring Test context framework provides first-class support for an integration test of a Spring-based application. We have defined lots of Spring managed beans in our web application context, such as services, repositories, view resolvers, and more, to run our application.

These managed beans are instantiated during the startup of an application by the Spring framework. While doing the integration testing, our test environment must also have those beans to test our application successfully. The Spring Test context framework gives us the ability to define a test context that is similar to the web application context. Let's see how to incorporate the Spring Test context to test our ProductValidator class.

Time for action - testing product validator

Let's see how we can boot up our test context using the Spring Test context framework to test our ProductValidator class:

  1. Open pom.xml, which you can find under the root directory of the project itself.
  2. You will be able to see some tabs at the bottom of the pom.xml file; select the Dependencies tab and click on the Add button of the Dependencies section.
  3. A Select Dependency window will appear; enter org.springframework as Group Id, spring-test as Artifact Id, 4.3.0.RELEASE as Version, and select test as Scope, then click on the OK button and save pom.xml.
  4. Similarly, add one more dependency for jsp-api; repeat the same step with Group Id as javax.servlet, Artifact Id as jsp-api, and Version as 2.0, but this time, select Scope as test and then click on the OK button and save pom.xml.
  5. Next create a class called ProductValidatorTest under the com.packt.webstore.validator package in the src/test/java source folder, and add the following code to it:
          package com.packt.webstore.validator; 
     
          import java.math.BigDecimal; 
     
          import org.junit.Assert; 
          import org.junit.Test; 
          import org.junit.runner.RunWith; 
          import org.springframework.beans.factory 
          .annotation.Autowired; 
          import org.springframework.test 
          .context.ContextConfiguration; 
          import org.springframework.test.context 
          .junit4.SpringJUnit4ClassRunner; 
          import org.springframework.test 
          .context.web.WebAppConfiguration; 
          import org.springframework.validation.BindException; 
          import org.springframework 
          .validation.ValidationUtils; 
     
          import com.packt.webstore.config 
          .WebApplicationContextConfig; 
          import com.packt.webstore.domain.Product; 
     
          @RunWith(SpringJUnit4ClassRunner.class) 
          @ContextConfiguration(classes =  
          WebApplicationContextConfig.class) 
          @WebAppConfiguration 
          public class ProductValidatorTest { 
         
              @Autowired 
              private ProductValidator productValidator; 
          
              @Test 
              public void  
          product_without_UnitPrice_should_be_invalid() { 
                  //Arrange 
                  Product product = new Product(); 
                  BindException bindException = new  
          BindException(product, " product"); 
     
                  //Act 
                  ValidationUtils.invokeValidator 
           (productValidator, product, bindException); 
             
                  //Assert 
                  Assert.assertEquals(1,  
          bindException.getErrorCount());  
                  Assert.assertTrue(bindException 
          .getLocalizedMessage().contains("Unit price is  
          Invalid. It cannot be empty.")); 
              } 
         
              @Test 
              public void  
          product_with_existing_productId_invalid() { 
                  //Arrange 
                  Product product = new Product("P1234","iPhone  
          5s", new BigDecimal(500)); 
                  product.setCategory("Tablet"); 
             
                  BindException bindException = new  
          BindException(product, " product"); 
     
                  //Act 
                  ValidationUtils.invokeValidator 
           (productValidator, product, bindException); 
             
                  //Assert 
                  Assert.assertEquals(1,  
          bindException.getErrorCount());  
                  Assert.assertTrue(bindException. 
          getLocalizedMessage().contains("A product already  
          exists with this product id.")); 
              } 
         
              @Test 
              public void a_valid_product_should_not_get 
          _any_error_during_validation() { 
                  //Arrange 
                  Product product = new Product("P9876","iPhone 
          5s", new BigDecimal(500)); 
                  product.setCategory("Tablet"); 
             
                  BindException bindException = new  
          BindException(product, " product"); 
     
                  //Act 
                  ValidationUtils.invokeValidator 
                 (productValidator, product, bindException); 
             
                  //Assert 
                  Assert.assertEquals(0, 
          bindException.getErrorCount());  
              } 
     
          } 
    
  6. Now right-click on ProductValidatorTest and choose Run As | JUnit Test. You will be able to see passing test cases, as shown in the following screenshot:
    Time for action - testing product validator

    Customer details collection form

What just happened?

As I already mentioned, Spring provides extensive support for integration testing. In order to develop a test case using the Spring Test context framework, we need the required spring-test JAR. In step 3, we just added a dependency to the spring-test JAR. The Spring Test context framework cannot run without the support of the JUnit JAR.

Step 5 is very important because it represents the actual test class (ProductValidatorTest) to test the validity of our Product domain object. The goal of the test class is to check whether all the validations (including bean validation and Spring validation) that were specified in the Product domain class are working. I hope you remember that we specified some of the bean validation annotations such as @NotNull, @Pattern, and more in the Product domain class.

One way to test whether those validations are taking place is by manually running our application and trying to enter invalid values. This approach is called manual testing. This is a very difficult job, whereas in automated testing we can write some test classes to run test cases in repeated fashion to test our functionality. Using JUnit, we can write this kind of test class.

The ProductValidatorTest class contains three test methods in total; we can identify a test method using the @Test (org.junit.Test) annotation of JUnit. Every test method can be logically separated into three parts, that is, Arrange, Act, and Assert. In the Arrange part, we instantiated and instrumented the required objects for testing; in the Act part, we invoked the actual functionality that needs to be tested; and finally in the Assert part, we compared the expected result and the actual result that is an output of the invoked functionality:

@Test 
public void product_without_UnitPrice_should_be_invalid() { 
        //Arrange 
        Product product = new Product(); 
        BindException bindException = new BindException(product, " product"); 
 
        //Act 
        ValidationUtils.invokeValidator(productValidator, product, bindException); 
         
        //Assert 
        Assert.assertEquals(1, bindException.getErrorCount());  
        Assert.assertTrue(bindException.getLocalizedMessage().contains("Unit price is Invalid. It cannot be empty.")); 
} 

In the Arrange part of this test method, we just instantiated a bare minimum Product domain object. We have not set any values for the productId, unitPrice, and category fields. We purposely set up such a bare minimum domain object in the Arrange part to check whether our ProductValidator class is working properly in the Act part.

According to the ProductValidator class logic, the present state of the product domain object is invalid. And in the Act part, we invoked the productValidator method using the ValidationUtils class to check whether the validation works or not. During validation, productValidator will store the errors in a BindException object. In the Arrange part, we simply check whether the bindException object contains one error using the JUnit Assert APIs, and check that the error message was as expected.

Another important thing you need to understand in our ProductValidatorTest class is that we used a Spring standard @Autowired annotation to get the instance of ProductValidator. The question here is who instantiated the productValidator object? The answer is in the @ContextConfiguration annotation. Yes, if you looked at the classes attribute specified in the @ContextConfiguration annotation, it has the name of our test context file (WebApplicationContextConfig.class).

If you remember correctly, you learned in the past that during the booting up of our application, Spring MVC creates a web application context (Spring container) with the necessary beans, as defined in the web application context configuration file. We need a similar kind of context even before running our test classes, so that we can use those defined beans (objects) in our test class to test them properly. The Spring Test framework makes this possible via the @ContextConfiguration annotation.

Likewise, we need a similar running application environment with all the resource files, and to achieve this we used the @WebAppConfiguration annotation from the Spring Test framework. The @WebAppConfiguration annotation instructs the Spring Test framework to load the application context as WebApplicationContext.

Now you have seen almost all the important things related to executing a Spring integration test, but there is one final configuration you need to understand, how to integrate JUnit and the Spring Test context framework into our test class. The @RunWith(SpringJUnit4ClassRunner.class) annotation just does this job.

So finally, when we run our test cases, we are able to see a green bar in the JUnit window indicating that the tests were successful.

Time for action - testing product Controllers

Now let's look at how to test our Controllers:

  1. Create a class called ProductControllerTest under the com.packt.webstore.controller package in the src/test/java source folder and add the following code to it:
          package com.packt.webstore.controller; 
     
          import static org.springframework.test.web.servlet 
          .request.MockMvcRequestBuilders.get; 
          import static org.springframework.test.web.servlet 
          .result.MockMvcResultMatchers.model; 
     
          import java.math.BigDecimal; 
     
          import org.junit.Before; 
          import org.junit.Test; 
          import org.junit.runner.RunWith; 
          import org.springframework.beans.factory 
          .annotation.Autowired; 
          import org.springframework.test.context 
          .ContextConfiguration; 
          import org.springframework.test.context 
          .junit4.SpringJUnit4ClassRunner; 
          import org.springframework.test.context 
          .web.WebAppConfiguration; 
          import org.springframework.test.web 
          .servlet.MockMvc; 
          import org.springframework.test.web 
          .servlet.setup.MockMvcBuilders; 
          import org.springframework.web 
          .context.WebApplicationContext; 
     
          import com.packt.webstore.config 
          .WebApplicationContextConfig; 
          import com.packt.webstore.domain.Product; 
     
          @RunWith(SpringJUnit4ClassRunner.class) 
          @ContextConfiguration(classes =  
          WebApplicationContextConfig.class) 
          @WebAppConfiguration 
          public class ProductControllerTest { 
     
              @Autowired 
              private WebApplicationContext wac; 
     
              private MockMvc mockMvc; 
     
              @Before 
              public void setup() { 
                  this.mockMvc =  
          MockMvcBuilders.webAppContextSetup(this.wac).build(); 
              } 
     
              @Test 
              public void testGetProducts() throws Exception { 
                  this.mockMvc.perform(get("/market/products")) 
                        .andExpect(model(). 
          attributeExists("products")); 
              } 
               
              @Test 
              public void testGetProductById() throws Exception  
          { 
                  //Arrange 
            Product product = new Product("P1234","iPhone 5s",        
          new BigDecimal(500)); 
             
                  //Act & Assert 
                  this.mockMvc.perform(get("/market/product") 
                                      .param("id", "P1234")) 
             .andExpect(model().attributeExists("product")) 
             .andExpect(model().attribute("product", product)); 
              } 
     
          } 
    
  2. Now right-click on the ProductControllerTest class and choose Run As | JUnit Test. You will be able to see that the test cases are being executed, and you will be able to see the test results in the JUnit window.

What just happened?

Similar to the ProductValidatorTest class, we need to boot up the test context and want to run our ProductControllerTest class as a Spring integration test. So we used similar annotations on top of ProductControllerTest as follows:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration(classes = WebApplicationContextConfig.class) 
@WebAppConfiguration 
public class ProductControllerTest { 

As well as the two test methods that are available under ProductControllerTest, a single setup method is available as follows:

@Before 
 public void setup() { 
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); 
 } 

The @Before annotation that is present on top the previous method indicates that this method should be executed before every test method. And within that method, we simply build our mockMvc object in order to use it in the following test methods. The MockMvc class is a special class provided by the Spring Test context framework to simulate browser actions within a test case, such as firing HTTP requests:

@Test 
public void testGetProducts() throws Exception { 
        this.mockMvc.perform(get("/market/products")) 
                    .andExpect(model().attributeExists("products")); 
} 

This test method simply fires a GET HTTP request to our application using the mockMvc object, and as a result, we ensure the returned model contains an attribute named products. Remember that our list method from the ProductController class is the thing handling the previous web request, so it will fill the model with the available products under the attribute name products.

After running our test case, you are able to see the green bar in the JUnit window, which indicates that the tests passed.

Time for action - testing REST Controllers

Similarly, we can test the REST-based Controllers as well—just follow these steps:

  1. Open pom.xml, which you can find pom.xml under the root directory of the project itself.
  2. You will be able to see some tabs at the bottom of pom.xml file; select the Dependencies tab and click on the Add button of the Dependencies section.
  3. A Select Dependency window will appear; for Group Id enter com.jayway.jsonpath, for Artifact Id enter json-path-assert, for Version enter 2.2.0, and for Scope select test, then click on the OK button and save pom.xml.
  4. Now create a class called CartRestControllerTest under the com.packt.webstore.controller package in the src/test/java source folder, and add the following code to it:
          package com.packt.webstore.controller; 
     
          import static org.springframework.test.web 
          .servlet.request.MockMvcRequestBuilders.get; 
          import static org.springframework.test.web 
          .servlet.request.MockMvcRequestBuilders.put; 
          import static org.springframework.test.web 
          .servlet.result.MockMvcResultMatchers.jsonPath; 
          import static org.springframework.test.web 
          .servlet.result.MockMvcResultMatchers.status; 
          import org.junit.Before; 
          import org.junit.Test; 
          import org.junit.runner.RunWith; 
          import org.springframework.beans.factory 
          .annotation.Autowired; 
          import org.springframework.mock 
          .web.MockHttpSession; 
          import org.springframework.test.context 
          .ContextConfiguration; 
          import org.springframework.test 
          .context.junit4.SpringJUnit4ClassRunner; 
          import org.springframework.test 
          .context.web.WebAppConfiguration; 
          import org.springframework 
          .test.web.servlet.MockMvc; 
          import org.springframework.test.web 
          .servlet.setup.MockMvcBuilders; 
          import org.springframework.web.context 
          .WebApplicationContext; 
     
          import com.packt.webstore.config 
          .WebApplicationContextConfig; 
     
          @RunWith(SpringJUnit4ClassRunner.class) 
          @ContextConfiguration(classes =  
          WebApplicationContextConfig.class) 
          @WebAppConfiguration 
          public class CartRestControllerTest { 
         
              @Autowired 
              private WebApplicationContext wac; 
     
              @Autowired 
              MockHttpSession session; 
         
              private MockMvc mockMvc; 
     
              @Before 
              public void setup() { 
                  this.mockMvc =  
          MockMvcBuilders.webAppContextSetup(this.wac).build(); 
              } 
         
              @Test 
              public void  
          read_method_should_return_correct_cart_Json_object ()  
          throws Exception { 
                  //Arrange 
          this.mockMvc.perform(put("/rest/cart/add/P1234") 
          .session(session)) 
                              .andExpect(status().is(200)); 
             
                  //Act 
                  this.mockMvc.perform(get("/rest/cart/"+  
          session.getId()).session(session)) 
                              .andExpect(status().isOk()) 
                         
          .andExpect(jsonPath("$.cartItems[0]. 
          product.productId").value("P1234")); 
              } 
     
          } 
    
  5. Now right-click on CartRestControllerTest and choose Run As | JUnit Test. You will be able to see that the test cases are being executed, and you will be able to see the test results in the JUnit window.

What just happened?

While testing REST Controllers, we need to ensure that the web response for the given web request contains the expected JSON object. To verify that, we need some specialized APIs to check the format of the JSON object. The json-path-assert JAR provides such APIs. We added a Maven dependency to the json-path-assert JAR from steps 1 to 3.

In step 4, we created our CartRestControllerTest to verify that our CartRestController class works properly. The CartRestControllerTest class is very similar to ProductControllerTest—the only difference is the way we assert the result of a web request. In CartRestControllerTest, we have one test method to test the read method of the CartRestController class.

The read method of CartRestController is designed to return a cart object as a JSON object for the given cart ID. In CartRestControllerTest, we tested this behavior in the read_method_should_return_correct_cart_Json_object test method:

@Test 
    public void read_method_should_return_correct_cart_Json_object () throws Exception { 
        //Arrange 
        this.mockMvc.perform(put("/rest/cart/add/P1234").session(session)) 
                    .andExpect(status().is(200)); 
         
        //Act 
        this.mockMvc.perform(get("/rest/cart/"+ session.getId()).session(session)) 
                    .andExpect(status().isOk()) 
                    .andExpect(jsonPath("$.cartItems[0].product.productId").value("P1234")); 
    } 

In order to get a cart object for the given cart ID, we need to store the cart object in our cart repository first, through a web request. That is what we did in the Arrange part of the previous test method. The first web request we fired in the Arrange part adds a product domain object in the cart, whose ID is the same as the session ID.

In the Act part of the test case, we simply fired another REST based web request to get the cart object as JSON object. Remember we used the session ID as our cart ID to store our cart object, so while retrieving it, we need to provide the same session ID in the request URL. For this, we can use the mock session object given by the Spring Test framework. You can see that we auto-wired the session object in our CartRestControllerTest class:

this.mockMvc.perform(get("/rest/cart/"+ session.getId()).session(session)) 
                .andExpect(status().isOk()) 
                .andExpect( jsonPath("$.cartItems[0].product.productId").value("P1234")); 

After we get the cart domain object as the JSON object, we have to verify whether it contains the correct product. We can do that with the help of the jsonPath method of MockMvcResultMatchers, as specified in the previous code snippets. After sending the REST web request to go get the cart object, we verified that the response status is okay and we also verified that the JSON object contains a product with the ID P1234.

Finally, when we run this test case, you can see the test cases being executed, and you can see the test results in the JUnit window.

Have a go hero - adding tests for the remaining REST methods

It's good that we tested and verified the read method of CartRestController, but we have not tested the other methods of CartRestController. You can add tests for the other methods of CartRestController in the CartRestControllerTest class to get more familiar with the Spring Test framework.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.37.129