JUnit 5 tutorial, part 2: Unit testing Spring MVC with JUnit 5

Unit test a Spring MVC service, controller, and repository with JUnit 5, Mockito, MockMvc, and DBUnit

1 2 3 4 Page 4
Page 4 of 4

Listing 8. The Spring repository test class (WidgetRepositoryTest.java)


package com.geekcap.javaworld.spring5mvcexample.repository;

import com.geekcap.javaworld.spring5mvcexample.model.Widget;
import com.github.database.rider.core.api.connection.ConnectionHolder;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.DBUnitExtension;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.sql.DataSource;
import java.util.List;
import java.util.Optional;

@ExtendWith(DBUnitExtension.class)
@SpringBootTest
@ActiveProfiles("test")
public class WidgetRepositoryTest {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private WidgetRepository repository;

    public ConnectionHolder getConnectionHolder() {
        return () -> dataSource.getConnection();
    }

    @Test
    @DataSet("widgets.yml")
    void testFindAll() {
        List<Widget> widgets = Lists.newArrayList(repository.findAll());
        Assertions.assertEquals(2, widgets.size(), "Expected 2 widgets in the database");
    }

    @Test
    @DataSet("widgets.yml")
    void testFindByIdSuccess() {
        Optional<Widget> widget = repository.findById(1L);
        Assertions.assertTrue(widget.isPresent(), "We should find a widget with ID 1");

        Widget w = widget.get();
        Assertions.assertEquals(1, w.getId(), "The widget ID should be 1");
        Assertions.assertEquals("Widget 1", w.getName(), "Incorrect widget name");
        Assertions.assertEquals("This is widget 1", w.getDescription(), "Incorrect widget description");
        Assertions.assertEquals(1, w.getVersion(), "Incorrect widget version");
    }

    @Test
    @DataSet("widgets.yml")
    void testFindByIdNotFound() {
        Optional<Widget> widget = repository.findById(3L);
        Assertions.assertFalse(widget.isPresent(), "A widget with ID 3 should not be found");
    }

    @Test
    @DataSet("widgets.yml")
    void testFindWidgetsWithNameLike() {
        List<Widget> widgets = repository.findWidgetsWithNameLike("Widget%");
        Assertions.assertEquals(2, widgets.size());
    }
}

The WidgetRepositoryTest class contains three annotations:

  • @SpringBootTest: Is the normal annotation we're familiar with that brings in the @ExtendWtih(SpringExtension.class) annotation to enable JUnit 5 integration.
  • @ActiveProfiles("test"): Tells Spring that this class should use all configurations that are annotated with @Profile("test") (including the test configuration created in Listing 7, which creates a custom DataSource).
  • @ExtendWith(DBUnitExtension.class): Brings in the DBUnit framework that will set up the database and populate and restore the database before and after every test.

The @DBUnitExtension needs a database connection to use to populate and clean up the database. To set up a database connection, our test class needs to provide a getConnectionHolder() method that returns a function that returns a connection. This is easy enough to implement: Ask Spring to autowire in our DataSource and then use it to return a connection to its database.

Test methods

With that done, let's review the first test method: testFindAll(). The first thing to notice is that the testFindAll() method is annotated with the @DataSet annotation, specifying that DBUnit should populate the database with the contents of the "widgets.yml" file before running the test. The DBUnitExtension expects its YAML files to be stored in "/src/test/resources/datasets". Listing 9 shows the contents of the src/test/resources/datasets/widgets.yml file.

Listing 9. widgets.yml

widget:
  - id: 1
    name: "Widget 1"
    description: "This is widget 1"
    version: 1
  - id: 2
    name: "Widget 2"
    description: "This is widget 2"
    version: 7

A YAML file follows a strict format and indentation matters. In this case, we specify that the table we want to insert our data into is named "widget," and then we want to insert two rows: one for "Widget 1" and one for "Widget 2". We have not reviewed the schema for our database yet, but a schema.sql file in the src/main/resources directory defines the schema. Listing 10 shows the schema.

Listing 10. Schema for the H2 database (schema.sql)


CREATE TABLE IF NOT EXISTS widget (
    id  INTEGER NOT NULL AUTO_INCREMENT,
    name VARCHAR(128) NOT NULL,
    description VARCHAR(256) NOT NULL,
    version INTEGER NOT NULL,
    PRIMARY KEY (id)
);

The widgets.yml maps its field names to the column names in the widget table.  Defining @DataSet("widgets.yml") ensures that, before the test runs, there will be two Widgets in the database with the fields defined in Listing 9.

The testFindAll() method invokes the WidgetRepository's findAll() method and validates that it returns the two records from the database.

The testFindByIdSuccess() method uses the same data set, queries for a record that should exist in the database, and then validates that it is present and that it has the correct values.

The testFindByIdNotFound() method queries for a record that does not exist in the database and validates that it is not found.

Finally, the testFindWithNameLike() method exercises our custom query and validates that it returns the expected records.

The DBUnit @DataSet annotation loads the contents of the YAML file into the database before running a test using the default configuration, which wipes the database between every test invocation. We can also specify a strategy configuration to change that behavior. Valid strategy values include:

  • INSERT: Inserts the rows in the YAML file into the database tables, but will leave existing rows unaffected
  • REFRESH: Updates matching rows in the YAML file, inserts missing rows, and leaves existing rows unaffected
  • UPDATE: Updates existing rows in the data set, but if there are rows in the YAML file that are not currently in the database then it throws an exception
  • CLEAN_INSERT (default value): Cleans the database and inserts the data set from the YAML file into the database

The CLEAN_INSERT strategy is the most expensive option, but it also ensures that you have a known set of data for repository interactions. Because of this, the order in which the tests run doesn't matter. There will not be any lingering items in the database from other test cases because everything is made new before each test runs. Unless you have a very large data set or so many tests that it becomes too unwieldy to rebuild the database every time, the CLEAN_INSERT strategy is the safest.

Conclusion

This tutorial has been an introduction to writing JUnit 5 unit tests for a Spring MVC application. We built on the examples from Part 1. You've seen how to integrate JUnit 5 and Mockito with Spring 5, and how to use these two test frameworks together to write tests against Spring services, controllers, and repositories. I also showed you how to use Spring's MockMvc utility to perform web requests against a controller, and how to integrate DBUnit into your test environment and run repository tests against an in-memory database.

There's a lot more you can do when testing Spring web services with JUnit 5. This tutorial is a good foundation for learning more. For more advanced scenarios, and several more tools that integrate well with JUnit 5 and Spring 5, see my Pluralsight course: TDD with Spring and JUnit 5.

This story, "JUnit 5 tutorial, part 2: Unit testing Spring MVC with JUnit 5 " was originally published by JavaWorld.

Copyright © 2020 IDG Communications, Inc.

1 2 3 4 Page 4
Page 4 of 4