Testing a RESTful Web Service in Spring for beginners ☕

|

Spring MVC provides good support for testing your RESTful web services. The spring-boot-starter-test dependency is convenient to use, it combines a compatible collection of testing libraries that offer the power to write short tests without needing to know a lot about each library.

Maven dependency

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

I should mention that people have variations on how they use these libraries, and have some different preferences on how they like to test. So, you may see some different approaches, and contradictory opinions! The goal is to find a way that you understand, and are comfortable with. So, I hope this is it!

What am I testing?

We can define 2 broad levels of testing for our application:

  1. Unit tests: we want to test each class/unit in isolation by excluding the surrounding infrastructure, and mocking dependencies. True unit tests typically run extremely quickly.
  2. Integration testing: we test everything working together, no mocking. You may also see this referred to as end-to-end testing, but some testing strategies consider end-to-end testing as a more complete testing stage.

I summarised how I define them in more detail below:

Unit test Integration test
A single class/unit is tested in isolation We test everything together.
Easy to write and verify. Setup of integration test might be complicated.
All dependencies are mocked if needed. No mocking is used (or only unrelated components are mocked).
Uses JUnit, a mocking framework, and maybe additional libraries for testing assertions. Can use same libraries as unit testing, but also may use a dedicated integration testing framework such as Arquillian or DbUnit.
Mostly used by developers. Useful to QA, DevOps, and Help Desk employees.
A failed unit test is always a regression (undesirable change in our code) if the business has not changed. A failed integration test can mean that the code is still correct, but the environment has changed.
Unit tests in an enterprise application should last about 5 minutes Integration tests in an enterprise application can last for hours.

Example Application

We will re-use our User example from this previous post. It has a model and a controller only, and has some default data inside the controller, which is there just for the purpose of demonstration, and wouldn’t be in a complete application.

How do I test?

Every test case should have following three steps:

  • Preparation: We set all data required to execute a method under test. You can include preparation that is common to every test method in a “setUp” method annotated with @Before (JUnit4) or @BeforeClass (JUnit5). Our test data is already inside the controller, so we skip this!
  • Execution: Execute the actual method under test. In our example, we will create a request which will cause a method in our controller class to be executed.
  • Verification: We check the expected behaviour of the method under test. For example, check if the response returned from the controller has the correct status code.

Often overlooked points for test cases are:

  • Independence: We want a test case to be self-contained. If data is modified in one test, it should not impact another test case. So, all test cases should begin in a known state. This means that it is probably necessary to write test initialization code that ensures that the external resource is in a known state.
  • Configuration should be minimal: We should not go too far with custom properties to replicate the exact conditions. If you want to do this, then this falls under end-to-end testing where the production environment is essentially replicated.

Unit testing

You can organize your test code in different ways.

Typically, in a maven project, your tests are put into src/test/java. Each test is named to follow a convention such as: class name + Test e.g. UserControllerTest.

Test class configuration

We need to do 2 things to set-up our test class:

  • add @RunWith(SpringRunner.class) to the test class: so it is identified as class to test by the test runner.
  • Create a StandaloneMockMVCBuilder: This builder creates the minimum infrastructure required to serve requests with the controllers we provide to. Before each test, this is run and creates a new UserController, this ensures we have a consistent state each time.
@RunWith(SpringRunner.class)
public class UserControllerTest {
    private MockMvc mockMvc;

    @Before
    public void setUp() {
        //create new controller for each test
        mockMvc = MockMvcBuilders
                .standaloneSetup(new UserController())
                .build();
    }

    //test methods

Other examples that you may have seen use a WebApplicationContext or other annotations that may load a complete application context, which uses more resources and gets further away from being a unit test.

You can see that it runs quickly. However, for some reason, it is a bit slow to return when something is not found!

unit test

Test methods

The slightly longer version is this, but may be clearer to understand the steps:

@Test
public void getAllUsers() throws Exception {
  //data already set up

  // execute
  MvcResult result = mockMvc
          .perform(MockMvcRequestBuilders.get("/users")
                  .accept(MediaType.APPLICATION_JSON_UTF8))
          .andReturn();

  // verify
  int status = result.getResponse().getStatus();
  assertEquals("Incorrect Response Status", HttpStatus.OK.value(), status);
  //more statements here to test JSON in response body
}

But you’re more likely to see this shortened syntax with methods chained together.

@Test
public void getAllUsers() throws Exception {
    mockMvc.perform(get("/users"))
            .andExpect(status().isOk())
            .andExpect(content().contentType("application/json;charset=UTF-8"))
            .andExpect(jsonPath("$", hasSize(3)))
            .andExpect(jsonPath("$[0].id").exists())
            .andExpect(jsonPath("$[0].name").exists())
            .andExpect(jsonPath("$[0].age").exists());
}

Static imports are used to allow us to call methods without an object. For example, static import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; enables us to create a mock GET request through get().

Some of the assertions use jsonPath() to validate the structure and contents of the JSON in the response body. $ is the root of the JSON and you can select fields with the dot notation.

The other test methods are similar to this and can be found in the source code.

Integration Testing

Each test can be named to follow a convention such as: class name + IT e.g. UserControllerIT. You can put them in the same folder as unit test if you want, or keep them separate.

Most integration tests are written for the top layer, in our case our controller.

Some tutorials such as this one on Baeldung.com use a WebApplicationContext and MockMVC, and exclude the web server. I will include an embedded web server, because my interpretation of integration testing is that you are testing how everything works together in a simple version of the real environment. It’s a small difference in the code, so you can decide for yourself!

Test class configuration

To perform integration tests for this application, we will create the following environment:

  • Deploy the application on Embedded Tomcat Server on a random port
  • Use TestRestTemplate from Spring Boot to call the Restful Web Services.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) //start web sever on random port
@TestPropertySource(locations = "classpath:test.properties") //if you want to include properties for database,logging..etc
public class UserControllerIT {

    @Autowired
    private TestRestTemplate restTemplate;

    private static final String URL = "/users/";

    //test methods
}

@TestPropertySource(locations = "classpath:test.properties") is optional. It loads the application properties for testing from the specified location. Here you can set logging, database settings, and so on.

Test methods

@Test
public void getUserById() throws Exception {
    // execute
    ResponseEntity<User> responseEntity = restTemplate.getForEntity("users/{id}", User.class, 1);

    // collect response
    int status = responseEntity.getStatusCodeValue();
    User resultUser= responseEntity.getBody();

    // verify
    assertEquals("Incorrect Response Status", HttpStatus.OK.value(), status);

    assertNotNull(resultUser);
    assertEquals(1, resultUser.getId());
}

The test case is very similar to the equivalent unit test, we are just using TestRestTemplate instead of MockMvc. Because we do not have other layers in our application, the additional part we are testing is the environment. But when you have an enterprise application there is usually other components such as: a repository layer; a database; and maybe a service layer, and in this case, you get more benefit from integration testing to see if they work together as expected.

Source code

Available here on github.

References

Related Posts