© Felipe Gutierrez 2016

Felipe Gutierrez, Pro Spring Boot, 10.1007/978-1-4842-1431-2_6

6. Testing with Spring Boot

Felipe Gutierrez

(1)Albuquerque, New Mexico, USA

This chapter shows you how to test your Spring Boot applications. It’s important to understand that you actually don’t need Spring to write tests, because you will write your classes following simple architectural design principles such as designing to an interface or using the SOLID object oriented design principle. Spring encourages you with some of these designs and provides some tools for testing.

Remember that Spring Boot is Spring, so testing should be very straight forward. You will reuse all the Spring test tools and features. In this case, you will use the spring-boot-starter-test pom for your unit and integration tests.

By default, the spring-boot-starter-test pom includes the Spring integration test for Spring applications, the JUnit, which is the de facto standard for unit testing Java applications, Objenesis, Hamcrest (a library of matcher objects), and Mockito (the Java mocking framework).

Testing Spring Boot

Let’s start by creating a test for a Spring application. Execute the following command in a terminal window:

$ spring init --package=com.apress.spring -g=com.apress.spring -a=spring-boot -name=sprint-boot -x

This command will create a Maven project. Take a look at the pom.xml shown in Listing 6-1.

Listing 6-1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>


        <groupId>com.apress.spring</groupId>
        <artifactId>spring-boot</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>


        <name>sprint-boot</name>
        <description>Demo project for Spring Boot</description>


        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.3.3.RELEASE</version>
                <relativePath/> <!-- lookup parent from repository -->
        </parent>


        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <java.version>1.8</java.version>
        </properties>


        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter</artifactId>
                </dependency>


                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>


        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>


</project>

Listing 6-1 shows the pom.xml for the project. Every time you create a project via Spring Initializr, you will get the spring-boot-starter-test pom by default. This will include spring-test, junit, hamcrest, objenesis, and mockito JARs. Of course, you can use Spock or another framework together with Spring test.

By default, the Spring Initializr includes a test class, as shown in Listing 6-2.

Listing 6-2. src/test/java/com/apress/spring/SpringBootApplicationTests.java
package com.apress.spring;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SprintBootApplication.class)
public class SprintBootApplicationTests {


        @Test
        public void contextLoads() {
        }


}

Listing 6-2 shows you the default test class. Let’s examine it:

  • @RunWith(SpringJUnit4ClassRunner.class). The @RunWith annotation belongs to the JUnit library and it will invoke the class it’s referencing (SpringJUnit4ClassRunner.class) to run the tests instead of the runner built into JUnit. The SpringJUnit4ClassRunner class is a custom extension of the JUnit’s BlockJUnit4ClassRunner. It provides all the functionality of the Spring Test Context Framework. The SpringJUnit4ClassRunner supports the following annotations:

    • @Test(expected=...)

    • @Test(timeout=...)

    • @Timed

    • @Repeat

    • @Ignore

    • @ProfileValueSourceConfiguration

    • @IfProfileValue

    You can also use the SpringClassRuleand SpringMethodRuleclasses, both a custom JUnit TestRule interface that supports class-level features of the TestContext Framework. They are used together with the @ClassRule and @Rule annotations.

  • @SpringApplicationConfiguration(classes = SprintBootApplication.class). This is a class-level annotation that knows how to load and configure an ApplicationContext, which means that you can have direct access to all the Spring container classes by just using the @Autowired annotation. In this case, the main SpringBootApplication class wires everything up.

  • @Test. This is a JUnit test annotation that will execute the method when the tests start. You can have one or more methods. If you have several methods with this annotation, it won’t execute them in order. For that you need to add the @FixMethodOrder(MethodSorters.NAME_ASCENDING)annotation to the class.

Web Testing

Let’s create a web project. This section shows you how to test web applications using third-party libraries. You can create a new directory ( spring-boot-web) and execute the following commands in a terminal window:

$ mkdir spring-boot-web
$ cd spring-boot-web
$ spring init -d=web,thymeleaf --package=com.apress.spring -g=com.apress.spring -a=spring-boot-web -name=sprint-boot-web -x

What is different about the previous project is that you are adding the -d=web,thymeleaf parameter, which will create a web project with the Thymeleaf technology as a view engine. The pom.xml file is shown in Listing 6-3.

Listing 6-3. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>


        <groupId>com.apress.spring</groupId>
        <artifactId>spring-boot-web</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <packaging>jar</packaging>


        <name>sprint-boot-web</name>
        <description>Demo project for Spring Boot</description>


        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.3.3.RELEASE</version>
                <relativePath/> <!-- lookup parent from repository -->
        </parent>


        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <java.version>1.8</java.version>
        </properties>


        <dependencies>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-thymeleaf</artifactId>
                </dependency>


                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>


        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>


</project>

Listing 6-3 shows you the pom.xmlfor a web project. As you can see, the dependencies are spring-boot-starter-web and spring-boot-starter-thymeleaf. Remember that by default the Spring Initializr will always bring the spring-boot-starter-test dependency. Next, take a look at the Java test-generated class shown in Listing 6-4.

Listing 6-4. src/test/java/com/apress/spring/SpringBootWebApplicationTests.java
package com.apress.spring;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SprintBootWebApplication.class)
@WebAppConfiguration
public class SpringBootWebApplicationTests {


        @Test
        public void contextLoads() {
        }


}

Listing 6-4 shows you the test class. Because the project is a web app, the tests include a new annotation called @WebAppConfiguration. It’s a class-level annotation that loads the org.springframework.web.context.WebApplicationContext implementation, which will ensure that all your files and beans related to the web app are accessible.

You are already familiar with the other annotations. Let’s create an example application that you can use for the next chapters. In the next chapter, you will extend the Spring Boot journal (by using the Spring Data module) by creating a RESTful API. For now, you will use the domain class and create “hard-coded” data.

Note

I recommend this particular article if you want to know more about the REST maturity model by Dr. Leonard Richardson. You can find it at Martin Fowler’s web site at http://martinfowler.com/articles/richardsonMaturityModel.html .

Let’s start by identifying the journal domain class. See Listing 6-5.

Listing 6-5. src/main/java/com/apress/spring/domain/JournalEntry.java
package com.apress.spring.domain;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;


public class JournalEntry {

        private String title;
        private Date created;
        private String summary;


        private final SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy");

        public JournalEntry(String title, String summary, String date) throws ParseException{
                this.title = title;
                this.summary = summary;
                this.created = format.parse(date);
        }


        JournalEntry(){}
        
        public String getTitle() {
                return title;
        }


        public void setTitle(String title) {
                this.title = title;
        }


        public Date getCreated() {
                return created;
        }


        public void setCreated(String date) throws ParseException{
                Long _date = null;
                try{
                        _date = Long.parseLong(date);
                        this.created = new Date(_date);
                        return;
                }catch(Exception ex){}
                this.created = format.parse(date);
        }


        public String getSummary() {
                return summary;
        }


        public void setSummary(String summary) {
                this.summary = summary;
        }


        public String toString(){
                StringBuilder value = new StringBuilder("* JournalEntry(");
                value.append("Title: ");
                value.append(title);
                value.append(",Summary: ");
                value.append(summary);
                value.append(",Created: ");
                value.append(format.format(created));
                value.append(")");
                return value.toString();
        }
}

Listing 6-5 shows you the domain class you will be using . I think the only thing to notice is that you will use a a small parsing process when you are setting the date (when you call setCreated) because you are passing the data as a string in a format of MM/dd/yyyy. If you pass a long type representing the timestamp, you can actually use the same setter. This is just for now; later in the book, you will see how this domain evolves.

Because you are going to test some RESTful endpoints, you need a controller. See Listing 6-6.

Listing 6-6. src/main/java/com/apress/spring/controller/JournalController.java
package com.apress.spring.controller;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;


import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


import com.apress.spring.domain.JournalEntry;

@RestController
public class JournalController {


  private static List<JournalEntry> entries = new ArrayList<JournalEntry>();
   static  {
     try {
     entries.add(new JournalEntry("Get to know Spring Boot","Today I will learn Spring Boot","01/01/2016"));
     entries.add(new JournalEntry("Simple Spring Boot Project","I will do my first Spring Boot Project","01/02/2016"));
     entries.add(new JournalEntry("Spring Boot Reading","Read more about Spring Boot","02/01/2016"));
     entries.add(new JournalEntry("Spring Boot in the Cloud","Spring Boot using Cloud Foundry","03/01/2016"));
    } catch (ParseException e) {
                e.printStackTrace();
    }
  }


  @RequestMapping("/journal/all")
  public List<JournalEntry> getAll() throws ParseException{
        return entries;
  }


  @RequestMapping("/journal/findBy/title/{title}")
  public List<JournalEntry> findByTitleContains(@PathVariable String title) throws ParseException{
        return entries
                .stream()
                      .filter(entry -> entry.getTitle().toLowerCase().contains(title.toLowerCase()))
                        .collect(Collectors.toList());
  }


  @RequestMapping(value="/journal",method = RequestMethod.POST )
   public JournalEntry add(@RequestBody JournalEntry entry){
        entries.add(entry);
        return entry;
   }

}

Listing 6-6 shows you the controller class. As you can see, you are going to have some journal entries in memory, and you are defining some endpoints:

  • /journal/all is where you will get all the journal entries in memory.

  • /journal/findBy/title/{title} is where you can search for some part of the title to get some results that match.

    These two endpoints correspond to the HTTP GET methods.

  • /journal – POST is where you will use the HTTP POST to add a new journal entry.

You already know about all the annotations used in this particular app, as they were discussed in the previous chapter. Next, you need to do your regular test and run the app to see if it works. You can run it with the following command:

$ ./mvnw spring-boot:run

Once it’s running you can go to http://localhost:8080/journal/all. You should see the JSON results like the ones shown in Figure 6-1.

A340891_1_En_6_Fig1_HTML.jpg
Figure 6-1. http://localhost:8080/journal/all

Figure 6-1 shows you the response you get by going to the /journal/all endpoint. Now, try the find endpoint. Look for the word “cloud”. The URL to visit will be http://localhost:8080/journal/findBy/title/cloud. You should see the results shown in Figure 6-2.

A340891_1_En_6_Fig2_HTML.jpg
Figure 6-2. http://localhost:8080/journal/findBy/title/cloud

Figure 6-2 shows you the result of going to the /journal/findBy/title/{title} endpoint. Next let’s try to post a new journal entry to the /journal endpoint. You can do that with the following command:

$ curl -X POST -d '{"title":"Test Spring Boot","created":"06/18/2016","summary":"Create Unit Test for Spring Boot"}' -H "Content-Type: application/json" http://localhost:8080/journal

This command shows you how to use the cURL UNIX command where you are posting a new journal entry in a JSON format to the /journal endpoint. Now you can go to /journal/all to see the new entry. See Figure 6-3.

A340891_1_En_6_Fig3_HTML.jpg
Figure 6-3. The /journal/all endpoint after inserting a new journal entry

Figure 6-3 shows you the new entry added by posting the JSON data to the /journal endpoint. Of course, this won’t cover testing. This was just an attempt to partially test. Although it might not make too much sense right now, imagine if you needed to add 1,000 records and you have even more endpoints to cover or you have different domain apps that need to go through all kinds of test.

Testing manually like you just did won’t work for the volume or for the application. That’s where unit and integration testing come in.

Before I talk about the unit test, you are going to use a library that is useful to test JSON objects. It’s called JsonPath by the company Jayway. So what you need to do is add the following dependency to your pom.xml:

<dependency>
        <groupId>com.jayway.jsonpath</groupId>
        <artifactId>json-path</artifactId>
        <scope>test</scope>
</dependency>

Because you are using the spring-boot-starter-test pom, you don’t need to specify the version. Now, let’s jump right into the new test you will be doing. See Listing 6-7.

Listing 6-7. src/test/java/com/apress/spring/SprintBootWebApplicationTests.java
package com.apress.spring;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;


import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;


import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.context.WebApplicationContext;


import com.apress.spring.domain.JournalEntry;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SprintBootWebApplication.class)
@WebAppConfiguration
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SprintBootWebApplicationTests {


        private final String SPRING_BOOT_MATCH = "Spring Boot";
        private final String CLOUD_MATCH = "Cloud";
        @SuppressWarnings("rawtypes")
        private HttpMessageConverter mappingJackson2HttpMessageConverter;
        private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
            MediaType.APPLICATION_JSON.getSubtype(),
            Charset.forName("utf8"));
        private MockMvc mockMvc;
        


        @Autowired
    private WebApplicationContext webApplicationContext;
        @Autowired
    void setConverters(HttpMessageConverter<?>[] converters) {
        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
                converter -> converter instanceof MappingJackson2HttpMessageConverter).findAny().get();
    }


        @Before
    public void setup() throws Exception {
        this.mockMvc = webAppContextSetup(webApplicationContext).build();
        }
        
        @Test
        public void getAll() throws Exception {
                mockMvc.perform(get("/journal/all"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$",iterableWithSize(5)))
                .andExpect(jsonPath("$[0]['title']",containsString(SPRING_BOOT_MATCH)));
        }


        @Test
        public void findByTitle() throws Exception {
                mockMvc.perform(get("/journal/findBy/title/" + CLOUD_MATCH))
                .andExpect(status().isOk())
                .andExpect(content().contentType(contentType))
                .andExpect(jsonPath("$",iterableWithSize(1)))
                .andExpect(jsonPath("$[0]['title']",containsString(CLOUD_MATCH)));
        }


        @Test
        public void add() throws Exception {
                mockMvc.perform(post("/journal")
                .content(this.toJsonString(new JournalEntry("Spring Boot Testing","Create Spring Boot Tests","05/09/2016")))
                .contentType(contentType)).andExpect(status().isOk());
        }


        @SuppressWarnings("unchecked")
        protected String toJsonString(Object obj) throws IOException {
        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
        this.mappingJackson2HttpMessageConverter.write(obj, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
        return mockHttpOutputMessage.getBodyAsString();
    }
}

Listing 6-7 shows you the unit test you will execute. Let’s examine it:

  • HttpMessageConverter<T>, MediaType, MockMvc, WebApplicationContext. The HttpMessageConverter<T> is an interface that helps to convert from and to HTTP requests and responses. You are going to use it to create a JSON format to post when you test. The MediaType instance specifies that the actual call will be a JSON object. The MockMvc is a helper class provided by the Spring MVC test module; you can get more information at http://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/integration-testing.html#spring-mvc-test-framework . The WebApplicationContext will provide the configuration for a web application and it will be necessary to create the MockMvc instance.

  • setConverters(HttpMessageConverter). This will set up the HttpMessageConverter<T> instance that is being used to convert the request, which in this example is when you post to the /journal endpoint to add a new entry. HttpMessageConverter<T> works for every HTTP method.

  • toJsonString(Object). This is a helper method that will write the actual journal entry to a JSON object.

  • setup(). This method is marked by the JUnit’s @Before annotation, which means that it will call the setup method for every test. In this case, it’s setting up the MockMvc instance to do some assertions on the code later.

  • getAll(). This method will test the /journal/all endpoint. As you can see, it’s using mockMvc to perform a HTTP GET method and it will assert that the status returned is the 200 CODE, that the response is a JSON object, and that the size returned of the collections is 5. You might wonder why this is 5 when there is only 4 in memory? I’ll show why next.

  • findByTitle(). This method will test the /journal/findBy/title/{title} endpoint. It will use the mockMvc instance to perform a get and it will assert that you have only one recurrence of a journal entry that includes the word “cloud”.

  • add(). This method will test the /journal endpoint by performing a POST using the mockMvc instance. It will assert that the content type is a JSON object (remember that you return the same object being posted) and that the status code is 200.

Why did you assert in the getAll method the size returned to 5? By default, the JUnit test methods are not running in sequence, which means that the getAll method can start first, then the add method, and so on. By default you don’t control that order. If you need to run your test in order, you can use the @FixMethodOrder(MethodSorters.NAME_ASCENDING) annotation, which tells the JUnit to run the test based on the method’s name in ascending order. This means that the add method will run first, then the getAll method, and finally the findByTitle method.

JsonPath together with the Hamcrest ( http://hamcrest.org/ ) libraries give you the flexibility to test RESTful APIs. You can get more information at https://github.com/jayway/JsonPath and learn what else you can do with this library.

If you export this project into the STS IDE, you can run the unit test and visualize it like in Figure 6-4.

A340891_1_En_6_Fig4_HTML.jpg
Figure 6-4. Running the tests using the STS IDE

You can create your unit tests using any framework you like. There is another project that makes testing your REST endpoint even easier. Go to https://github.com/jayway/rest-assured to find out more. The name of the library is Rest-Assured and it provides a fluent API to test RESTful APIs.

Summary

This chapter showed you how to test Spring and Spring Boot applications using the JUnit, using the provided MockMvc, and using other test libraries like Hamcrest and JsonPath.

In the following chapter, you learn about the persistence mechanism in Spring Boot and you will continue working with the journal app.

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

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