This lab explores automated testing at the unit and integration levels, and what Spring Boot brings to the table.

1. The testing "Starter"

➜ Review the demo project’s build dependencies:

build.gradle
dependencies {
  compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
  compile 'org.springframework.boot:spring-boot-devtools'

  compileOnly 'org.projectlombok:lombok'

  testCompile 'org.springframework.boot:spring-boot-starter-test'
}

The file build.gradle already contains a test dependency by the name of spring-boot-starter-test.

➜ Review the dependencies that this starter brings into the project:

$ gradle dependencies --configuration testCompile

Here’s the relevant output:

...
\--- org.springframework.boot:spring-boot-starter-test -> 2.0.3.RELEASE
     +--- org.springframework.boot:spring-boot-starter:2.0.3.RELEASE (*)
     +--- org.springframework.boot:spring-boot-test:2.0.3.RELEASE
     |    \--- org.springframework.boot:spring-boot:2.0.3.RELEASE (*)
     +--- org.springframework.boot:spring-boot-test-autoconfigure:2.0.3.RELEASE
     |    +--- org.springframework.boot:spring-boot-test:2.0.3.RELEASE (*)
     |    \--- org.springframework.boot:spring-boot-autoconfigure:2.0.3.RELEASE (*)
     +--- com.jayway.jsonpath:json-path:2.4.0
     |    +--- net.minidev:json-smart:2.3
     |    |    \--- net.minidev:accessors-smart:1.2
     |    |         \--- org.ow2.asm:asm:5.0.4
     |    \--- org.slf4j:slf4j-api:1.7.25
     +--- junit:junit:4.12
     |    \--- org.hamcrest:hamcrest-core:1.3
     +--- org.assertj:assertj-core:3.9.1
     +--- org.mockito:mockito-core:2.15.0
     |    +--- net.bytebuddy:byte-buddy:1.7.9 -> 1.7.11
     |    +--- net.bytebuddy:byte-buddy-agent:1.7.9 -> 1.7.11
     |    \--- org.objenesis:objenesis:2.6
     +--- org.hamcrest:hamcrest-core:1.3
     +--- org.hamcrest:hamcrest-library:1.3
     |    \--- org.hamcrest:hamcrest-core:1.3
     +--- org.skyscreamer:jsonassert:1.5.0
     |    \--- com.vaadin.external.google:android-json:0.0.20131108.vaadin1
     +--- org.springframework:spring-core:5.0.7.RELEASE (*)
     +--- org.springframework:spring-test:5.0.7.RELEASE
     |    \--- org.springframework:spring-core:5.0.7.RELEASE (*)
     \--- org.xmlunit:xmlunit-core:2.5.1

By using this one starter dependency, we get a number of useful testing libraries that we would otherwise have to include individually:

  • junit:junit: the canonical testing library for Java

  • org.springframework:spring-test: spring’s test support library

  • org.mockito:mockito-core: the canonical java mocking library

  • org.hamcrest:hamcrest-core: the well-known assertions library

  • org.assertj:assertj-core: an alternative and popular assertions library

  • org.skyscreamer:jsonassert: an assertions library for working with JSON objects

  • com.jayway.jsonpath:json-path: an expression language for JSON documents analogous to what XPath does for xml

2. Refactor TodoController

In the last lab, we introduced the domain object Todo.java and an accompanying controller, the TodoController, for the purpose of demonstrating the use of Spring Controllers with Template Engines.

That controller just inlined a hard-coded list of Todo items, and it served its purpose. Let’s properly refactor this code and separate the data concern from the controller.

The Repository pattern describes an object responsible for data operations for a given entity. The Spring Data project defines the CrudRepository interface, which we will discuss in a later lab. For now, let’s define our own TodoRepository, with a similar contract:

TodoRepository.java
package com.example.demo;

import java.util.List;
import java.util.Optional;

public interface TodoRepository {
  Todo save(Todo todo);
  void delete(Todo todo);

  List<Todo> findAll();
  long count();

  Optional<Todo> findById(Long id);
}

In the next lab, we’ll explore the Spring Data project, and integrate a Spring Boot application to a backing database. For now, let’s implement a simple in-memory version backed by a Java HashMap, the InMemoryTodoRepository.

➜ Create a class named InMemoryTodoRepository.java that implements the TodoRepository interface, with default implementations of all of the interface’s methods, and add a private Map field, perhaps named todoMap, to hold the Todo items in memory:

InMemoryTodoRepository.java
package com.example.demo;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public final class InMemoryTodoRepository implements TodoRepository {

  private final Map<Long, Todo> todoMap = new HashMap<>();

  @Override
  public List<Todo> findAll() {
    return null;
  }

  @Override
  public Todo save(Todo todo) {
    return null;
  }

  @Override
  public void delete(Todo todo) {

  }

  @Override
  public Optional<Todo> findById(Long id) {
    return Optional.empty();
  }

  @Override
  public long count() {
    return 0;
  }
}

The basic strategy is that each Todo item will have a unique id, of type Long, that will be used as the key in the HashMap.

➜ In Todo.java, add an id field of type Long:

Todo.java
 import lombok.Data;

 import java.time.LocalDate;

 @Builder
 @Data
 public class Todo {
+  private final Long id;
   private final String title, description;
   private final LocalDate dueDate;
 }

The implementation of InMemoryTodoRepository will be supported by tests. Unit tests are sufficient here; the Spring testing support isn’t needed yet. We’ll use JUnit and AssertJ.

If you’re not familiar with AssertJ and its assertions, take a little time right now to visit the web site, scan through the documentation, and familiarize yourself with it.

➜ In src/test/java/com/example/demo, create a test for this class, named InMemoryTodoRepositoryTest (in IntelliJ, Cmd+Shift+T will trigger this action), :

InMemoryTodoRepositoryTest.java
package com.example.demo;

import org.junit.Before;

public class InMemoryTodoRepositoryTest {

  private InMemoryTodoRepository repository;
  private Todo todo;

  @Before
  public void setup() {
    repository = new InMemoryTodoRepository();

    todo = Todo.builder()
        .title("My Todo")
        .description("it's urgent")
        .dueDate(LocalDate.now())
        .build();
  }

}

Above, we see the use of the Builder pattern (as implemented by the project Lombok library) that makes clear how the object is configured.

Lombok builders have a toBuilder option, for the purpose of creating an object instance patterned after another. Enable this feature:

Todo.java
@@ -5,9 +5,10 @@ import lombok.Data;

 import java.time.LocalDate;

-@Builder
+@Builder(toBuilder = true)
 @Data
 public class Todo {
   private final Long id;
   private final String title, description;
   private final LocalDate dueDate;
 }

2.1. Develop the Repository

Follow the red green refactor process of TDD to gradually develop a working implementation for InMemoryTodoRepository.

Here’s a first, simple test:

  @Test
  public void shouldBeInitiallyEmpty() {
    assertThat(repository.findAll()).isEmpty();
  }

Run it. Watch it fail. Make it pass.

Proceed to add another test until the complete TodoRepository interface is working to your satisfaction.

Here is my final test class implementation:

InMemoryTodoRepositoryTest.java
package com.example.demo;

import org.junit.Before;
import org.junit.Test;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

import static org.assertj.core.api.Assertions.assertThat;

public class InMemoryTodoRepositoryTest {

  private InMemoryTodoRepository repository;
  private Todo todo;

  @Before
  public void setup() {
    repository = new InMemoryTodoRepository();

    todo = Todo.builder()
        .title("My Todo")
        .description("it's urgent")
        .dueDate(LocalDate.now())
        .build();

  }

  @Test
  public void shouldBeInitiallyEmpty() {
    assertThat(repository.findAll()).isEmpty();
    assertThat(repository.count()).isEqualTo(0);
  }

  @Test
  public void shouldAddAnEntry() {
    Todo savedTodo = repository.save(todo);
    assertThat(savedTodo.getId()).isNotNull();
    assertThat(savedTodo.getTitle()).isEqualTo(todo.getTitle());
    assertThat(repository.count()).isEqualTo(1);
    assertThat(repository.findById(savedTodo.getId()).isPresent()).isTrue();
  }

  @Test
  public void shouldNotFindTodoWithAbsentId() {
    assertThat(repository.findById(123L).isPresent()).isFalse();
  }

  @Test
  public void shouldRemoveAnEntry() {
    Todo savedTodo = repository.save(todo);
    repository.delete(savedTodo);
    assertThat(repository.findAll()).isEmpty();
  }

  @Test
  public void shouldHandleDeleteATodoWithNoId() {
    repository.delete(todo);
    assertThat(repository.findAll()).isEmpty();
  }

  @Test
  public void shouldHandleDeleteATodoWithIdNotPresent() {
    Todo todoWithId = todo.toBuilder().id(17L).build();
    repository.delete(todoWithId);
    assertThat(repository.findAll()).isEmpty();
  }

  @Test
  public void shouldReplaceTodoWithSameId() {
    Todo savedTodo = repository.save(todo);

    Long id = savedTodo.getId();

    Todo anotherTodo = Todo.builder()
        .id(id)
        .title("Another Todo")
        .description("it's not as urgent")
        .dueDate(LocalDate.now().plus(1, ChronoUnit.DAYS))
        .build();

    repository.save(anotherTodo);

    assertThat(repository.count()).isEqualTo(1);
    assertThat(repository.findById(id).isPresent()).isTrue();
    assertThat(repository.findById(id).get().getTitle()).isEqualTo(anotherTodo.getTitle());
    assertThat(repository.findById(id).get().getDescription()).isEqualTo(anotherTodo.getDescription());
  }

  @Test
  public void shouldHoldMultipleEntries() {
    int n = 100;
    for (int i = 0; i < n; i++) {
      repository.save(Todo.builder()
          .title("title" + i)
          .description("description" + i)
          .dueDate(LocalDate.now()).build()
      );
    }
    assertThat(repository.count()).isEqualTo(n);
  }

}

And here’s the repository implementation:

InMemoryTodoRepository.java
package com.example.demo;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

public final class InMemoryTodoRepository implements TodoRepository {

  private final Map<Long, Todo> todoMap = new HashMap<>();

  private final AtomicLong sequence = new AtomicLong(0);

  @Override
  public Todo save(Todo todo) {
    if (todo.getId() == null) {
      Long id = sequence.incrementAndGet();
      Todo todoWithId = todo.toBuilder().id(id).build();
      todoMap.put(id, todoWithId);
      return todoWithId;
    } else {
      todoMap.replace(todo.getId(), todo);
      return todo;
    }
  }

  @Override
  public void delete(Todo todo) {
    todoMap.remove(todo.getId());
  }

  @Override
  public Optional<Todo> findById(Long id) {
    return Optional.ofNullable(todoMap.get(id));
  }

  @Override
  public long count() {
    return todoMap.size();
  }

  @Override
  public List<Todo> findAll() {
    return new ArrayList<>(todoMap.values());
  }
}

2.2. Expose the Repository Bean

Recall that the @SpringBootApplication annotation implies that our application class is also a Spring @Configuration class. This means we can define @Bean-annotated methods directly in this class.

➜ Expose a TodoRepository Spring bean for the application:

DemoApplication.java
@@ -3,6 +3,7 @@ package com.example.demo;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;

 @SpringBootApplication
 @EnableConfigurationProperties(HelloProperties.class)
@@ -11,4 +12,9 @@ public class DemoApplication {
   public static void main(String[] args) {
     SpringApplication.run(DemoApplication.class, args);
   }
+
+  @Bean
+  public TodoRepository todoRepository() {
+    return new InMemoryTodoRepository();
+  }
 }

2.3. Move Data Initialization to the Runner

Instead of constructing the Todo items directly in the TodoController, move that code to Runner.java, the CommandLineProcessor. This is the right place for initializing our application with a set of Todo items.

src/main/java/com/example/demo/Runner.java
@@ -5,20 +5,46 @@ import org.slf4j.LoggerFactory;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.stereotype.Component;

+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+
 @Component
 public class Runner implements CommandLineRunner {

   private final Logger logger = LoggerFactory.getLogger(Runner.class);

   private final HelloService helloService;
+  private final TodoRepository todoRepository;

-  public Runner(HelloService helloService) {
+  public Runner(HelloService helloService, TodoRepository todoRepository) {
     this.helloService = helloService;
+    this.todoRepository = todoRepository;
   }

   @Override
-  public void run(String... args) throws Exception {
+  public void run(String... args) {
     helloService.greet();
+
+    LocalDate nextWeek = LocalDate.now().plus(7, ChronoUnit.DAYS);
+
+    todoRepository.save(Todo.builder()
+        .title("Shop")
+        .description("Go shopping ahead of trip")
+        .dueDate(nextWeek).build());
+    todoRepository.save(Todo.builder()
+        .title("Pack")
+        .description("Be sure to pack your things")
+        .dueDate(nextWeek).build());
+    todoRepository.save(Todo.builder()
+        .title("Drive")
+        .description("Drive to the airport")
+        .dueDate(nextWeek).build());
+    todoRepository.save(Todo.builder()
+        .title("Fly")
+        .description("Fly to some mysterious destination")
+        .dueDate(nextWeek).build());
+
+
     logger.debug("exiting run method..");
   }
 }

Above, we:

  • Autowire the repository via the constructor.

  • Save the Todo items to the repository inside the run() method.

2.4. Retrofit TodoController endpoint

The only task remaining is to retrofit the TodoController to use the repository to fetch the Todo’s:

TodoController.java
package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Controller
public class TodoController {

  private final TodoRepository todoRepository;

  public TodoController(TodoRepository todoRepository) {
    this.todoRepository = todoRepository;
  }

  @GetMapping("/todos")
  public String fetchTodos(Model model) {

    List<TodoViewModel> todoViews = todoRepository
        .findAll()
        .stream()
        .map(TodoViewModel::new)
        .collect(Collectors.toList());

    model.addAttribute("todos", todoViews);

    return "todos"; // the name of the template to render
  }
}
  • Fire up the application and verify that the /todos endpoint still functions.

  • Using gradle, run all tests from the command line:

    $ gradle test
  • Inspect the generated HTML test report at build/reports/tests/test/index.html.

This completes the refactoring. Unit testing the Todo repository was fairly straightforward. Even for more complex classes that have delegates, mocks can make it easy to test an object under test in isolation, and under different sets of conditions.

Let’s turn our attention to Spring Boot’s test support by developing a REST API for Todo’s.

3. Developing a REST API for Todo’s

We have an endpoint that returns an HTML page listing Todos. We’d also like to have a REST API for creating, fetching, updating, and deleting Todos.

Let’s develop this from the outside in: by writing integration tests that expect these API endpoints to exist, and to behave in a certain way. We’ll then make the tests pass by implementing the endpoints in question.

➜ In src/test/java/com/example/demo, create a test named TodoRestControllerTests.java, as follows:

package com.example.demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.junit.Assert.fail;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@RunWith(SpringRunner.class) (1)
@SpringBootTest(webEnvironment = RANDOM_PORT) (2)
public class TodoRestControllerTests {

  @Test
  public void shouldFetchTodos() {
    // to be implemented shortly
  }

}
1 @RunWith tells JUnit to use the Spring runner for this test
2 @SpringBootTest will enlist the help of Spring Boot to start the application context, and to identify the main class for our application automatically. webEnvironment asks Spring to start the application on any available random port.

➜ Even though no test is yet written, go ahead and run this test class

You should see the Spring application start up; the empty test will pass.

The idea is to have Spring start our application, and then to run tests that make HTTP calls to the application from the outside. Spring’s RestTemplate is an ideal and simple REST client for this. Spring Boot provides TestRestTemplate, a version that already knows the base URL of the running application.

➜ Autowire a TestRestTemplate into the test, like this:

  @Autowired
  private TestRestTemplate template;

Here’s a stab at a first test:

  @Test
  public void shouldFetchTodos() {
    ResponseEntity<List<Todo>> response = template.exchange("/api/todos", HttpMethod.GET,
        null, new ParameterizedTypeReference<List<Todo>>() {}); (1)
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
  }
1 The ParamaterizedTypeReference is a mechanism for dealing with Java generics to unmarshal response JSON back to a list of Todo objects.

We expect an endpoint at /api/todos to return at least an HTTP 200 response.

  • Run this test

  • Watch it fail

  • Implement the code necessary to make it pass:

    • Create a new class: TodoRestController.java.

    • Annotate it with @RestController

    • Annotate the class with a @RequestMapping annotation with a specified route matching /api/todos

    • Autowire the class with a TodoRepository

    • Implement a simple @GetMapping-annotated handler method to return the Todo items

package com.example.demo;

import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/todos")
public class TodoRestController {

  private final TodoRepository repository;

  public TodoRestController(TodoRepository repository) {
    this.repository = repository;
  }

  @GetMapping
  public List<Todo> list() {
    return repository.findAll();
  }
}

Re-run the test; it should pass. Congratulations, you’ve implemented the first endpoint!

The goal is to proceed in a test-driven fashion to implement additional tests until we have a fully implemented Rest API:

  • GET api/todos to fetch the list

  • GET api/todos/{id} to fetch a single item by id

  • POST api/todos to create a new Todo

  • PUT api/todos/{id} for updates

  • DELETE api/todos/{id} to remove an entry

Given the full set of tests below, complete the implementation of TodoRestController and make them all pass.

TodoRestControllerTests.java
package com.example.demo;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class TodoRestControllerTests {

  @Autowired
  private TestRestTemplate template;

  @MockBean (1)
  private TodoRepository repository;
  private Todo todo1;

  @Before
  public void setup() {
    List<Todo> todos = new ArrayList<>();
    todo1 = Todo.builder().title("Todo1").id(1L).build();
    todos.add(todo1);
    Todo todo2 = Todo.builder().title("Todo2").id(2L).build();
    todos.add(todo2);
    when(repository.findAll()).thenReturn(todos);

    when(repository.findById(anyLong())).thenReturn(Optional.empty());
    when(repository.findById(1L)).thenReturn(Optional.of(todo1));
    when(repository.findById(2L)).thenReturn(Optional.of(todo2));

    when(repository.save(any(Todo.class))).thenReturn(todo1);
  }

  @Test
  public void shouldReturnTodos() {
    ResponseEntity<List<Todo>> response = template.exchange("/api/todos", HttpMethod.GET,
        null, new ParameterizedTypeReference<List<Todo>>() {});
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    List<Todo> todos = response.getBody();
    assertThat(todos.size()).isEqualTo(2);
  }

  @Test
  public void shouldReturnASingleTodo() {
    ResponseEntity<Todo> response = template.getForEntity("/api/todos/1", Todo.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    Todo todo = response.getBody();
    assertThat(todo.getTitle()).isEqualTo("Todo1");
  }

  @Test
  public void shouldNotFindTodoId3() {
    ResponseEntity<Todo> response = template.getForEntity("/api/todos/3", Todo.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
  }

  @Test
  public void shouldSaveATodo() {
    ResponseEntity<Todo> response = template.postForEntity("/api/todos", todo1, Todo.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    Todo todo = response.getBody();
    assertThat(todo.getTitle()).isEqualTo("Todo1");
  }

  @Test
  public void shouldDeleteATodo() {
    ResponseEntity<Todo> response =
        template.exchange("/api/todos/1", HttpMethod.DELETE, null, Todo.class);
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
  }

}
1 The @MockBean annotation is another Spring Boot convenience that not only mocks an object, but wires the mock into all Spring beans that require it. In this case, I’ve decided that the test should test only the controller, not the repository: the repository is mocked and instrumented to return Todo instances fabricated by the test. i.e. this test is in full control of the data setup.

Run all tests, watch them fail, and, one by one, make them pass. For reference, here is a completed implementation of the controller:

TodoRestController.java
package com.example.demo;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/todos")
public class TodoRestController {

  private final TodoRepository repository;

  public TodoRestController(TodoRepository repository) {
    this.repository = repository;
  }

  @GetMapping
  public List<Todo> list() {
    return repository.findAll();
  }

  @GetMapping("{id}")
  public ResponseEntity<Todo> get(@PathVariable Long id) {
    Optional<Todo> optional = repository.findById(id);
    return optional.isPresent() ?
        ResponseEntity.ok(optional.get()) : ResponseEntity.notFound().build();
  }

  @PostMapping
  public ResponseEntity<Todo> create(@RequestBody Todo todo) {
    Todo savedTodo = repository.save(todo);
    return new ResponseEntity<>(savedTodo, HttpStatus.CREATED);
  }

  @DeleteMapping("{id}")
  public ResponseEntity<Todo> delete(@PathVariable Long id) {
    Optional<Todo> optional = repository.findById(id);
    if (optional.isPresent()) {
      repository.delete(optional.get());
    }
    return ResponseEntity.noContent().build();
  }

}

Once you have all tests passing, fire up the application and exercise the GET endpoints directly from a browser. For HTTP POST and other verbs, you’ll need a tool such as Postman or a command-line HTTP client such as curl or HTTPie.

There’s more to Spring’s support for integration tests.

  • MockMvc supports controller integration testing in a lighter-weight fashion, without actually starting a web server.

  • JsonPath provides a DSL for validating JSON responses.

  • Spring Boot offers support for partial construction of an application context for testing slices of your application.

To find out more about automated test support in Spring Boot, check out: