Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Tests with @SpringBootTest

In the tutorial you wrote JUnit tests for plain classes. The blog service cannot be tested that way: it needs repositories, a database and a Clock. The @SpringBootTest annotation starts the whole Spring container in a test — just as at startup, only without opening a port. You can get beans right into the test class.

What is already there

The generator put the first test into the project, and test starters into pom.xml (spring-boot-starter-webmvc-test and the other -test ones), which contain JUnit 5, AssertJ and Mockito.

// src/test/java/com/example/blog/BlogApplicationTests.java
@SpringBootTest
@ActiveProfiles("test")
class BlogApplicationTests {
 
    @Test
    void contextLoads() {
    }
}

The empty test is not as useless as it looks: it passes only if the whole application starts — all beans created, settings read, migrations applied.

A database for tests

Tests must not touch the data/blog.mv.db file. Let's create a test profile with an in-memory database:

# src/test/resources/application-test.yaml
spring:
  datasource:
    url: jdbc:h2:mem:blog-test
  jpa:
    show-sql: false

@ActiveProfiles("test") switches this profile on. A mem: database lives while the test process runs; Flyway creates the tables in it at startup.

A service test

// src/test/java/com/example/blog/PostServiceTests.java
package com.example.blog;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
 
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class PostServiceTests {
 
    @Autowired
    private PostService postService;
 
    @Autowired
    private UserRepository users;
 
    @Test
    void createsPostWithAuthor() {
        users.save(new User("anna", "{noop}changeme"));
 
        PostResponse post = postService.create(new NewPostRequest("Hello", "Text"), "anna");
 
        assertThat(post.id()).isNotNull();
        assertThat(post.author()).isEqualTo("anna");
    }
 
    @Test
    void throwsWhenPostIsMissing() {
        assertThatThrownBy(() -> postService.findById(999L))
                .isInstanceOf(PostNotFoundException.class)
                .hasMessage("Post 999 not found");
    }
}

In tests beans are received with @Autowired on a field — JUnit, not Spring, calls the test class constructor. {noop} is the "unhashed password" prefix, acceptable for test data. @Transactional on the test class rolls the transaction back after each test, and the database is empty again.

Running

./mvnw test
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.049 s -- in com.example.blog.PostServiceTests
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

The first test in a run takes about ten seconds — Spring starts the application. Other classes with the same settings reuse the ready container, so they are fast. ./mvnw package also runs the tests and does not build the JAR if they fail.

Common mistake

Without @Transactional the data of one test stays in the database for the next ones. A test that creates Anna in @BeforeEach fails from the second time on:

[ERROR] Tests run: 4, Failures: 0, Errors: 3, Skipped: 0 <<< FAILURE! -- in com.example.blog.PostControllerTests
Unique index or primary key violation: "PUBLIC.CONSTRAINT_4D INDEX PUBLIC.CONSTRAINT_4D_INDEX_6 ON PUBLIC.USERS(USERNAME NULLS FIRST) VALUES ( /* 1 */ 'anna' )"

The user anna already exists, and the name is unique. Add @Transactional to the test class — and each test starts with a clean database.

Thanks for reading!

Donate to the site — we work hard for you.