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 SUCCESSThe 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.