After reading the JUnit doc again, I found it is good to use a @Nested
inner class to group tests and finally display them in a tree structure in the test reports.
But when refactoring my PostController
like this.
@WebFluxTest(
controllers = PostController.class,
excludeAutoConfiguration = {
ReactiveUserDetailsServiceAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class
}
)
@Slf4j
@DisplayName("testing /posts endpoint")
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public class PostControllerTest {
@Autowired
private WebTestClient client;
@MockBean
private PostRepository posts;
@MockBean
private CommentRepository comments;
@BeforeAll
public static void beforeAll() {
log.debug("before all...");
}
@AfterAll
public static void afterAll() {
log.debug("after all...");
}
@BeforeEach
public void beforeEach() {
log.debug("before each...");
}
@AfterEach
public void afterEach() {
log.debug("after each...");
}
@Nested
@DisplayName("/posts GET")
class GettingAllPosts {
@Test
@DisplayName("should return 200 when getting posts with keyword")
void shouldBeOkWhenGettingPostsWithKeyword() {
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdDate"));
given(posts.findByTitleContains("first", pageRequest))
.willReturn(Flux.just(
Post.builder()
.id("1")
.title("my first post")
.content("content of my first post")
.createdDate(LocalDateTime.now())
.status(Post.Status.PUBLISHED)
.build()
)
);
client.get().uri("/posts?q=first")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].title").isEqualTo("my first post")
.jsonPath("$[0].id").isEqualTo("1")
.jsonPath("$[0].content").isEqualTo("content of my first post");
verify(posts, times(1)).findByTitleContains(anyString(), any(Pageable.class));
verifyNoMoreInteractions(posts);
}
@Test
@DisplayName("should return 200 when getting posts without keyword")
void shouldBeOkWhenGettingPostsWithoutKeyword() {
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdDate"));
given(posts.findAll(pageRequest.getSort()))
.willReturn(
Flux.just(
Post.builder()
.id("1")
.title("my first post")
.content("content of my first post")
.createdDate(LocalDateTime.now())
.status(Post.Status.PUBLISHED)
.build()
)
);
client.get().uri("/posts")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].title").isEqualTo("my first post")
.jsonPath("$[0].id").isEqualTo("1")
.jsonPath("$[0].content").isEqualTo("content of my first post");
verify(posts, times(1)).findAll(any(Sort.class));
verifyNoMoreInteractions(posts);
}
@Test
@DisplayName("should return 200 when getting posts with keyword and pagination")
void shouldBeOkWhenGettingPostsWithKeywordAndPagiantion() {
List<Post> data = IntStream.range(1, 11)// 15 posts will be created.
.mapToObj(n -> Post.builder().id("" + n).title("my " + n + " blog post")
.content("content of my " + n + " blog post").status(Post.Status.PUBLISHED)
.createdDate(LocalDateTime.now()).build())
.collect(toList());
List<Post> data2 = IntStream.range(11, 16)// 5 posts will be created.
.mapToObj(n -> Post.builder().id("" + n).title("my " + n + " blog test post")
.content("content of my " + n + " blog post").status(Post.Status.PUBLISHED)
.createdDate(LocalDateTime.now()).build())
.collect(toList());
PageRequest pageRequest = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "createdDate"));
PageRequest pageRequest2 = PageRequest.of(1, 10, Sort.by(Sort.Direction.DESC, "createdDate"));
given(posts.findAll(pageRequest.getSort())).willReturn(Flux.fromIterable(data));
given(posts.findByTitleContains("test", pageRequest2)).willReturn(Flux.fromIterable(data2));
given(posts.count()).willReturn(Mono.just(15L));
given(posts.countByTitleContains("5")).willReturn(Mono.just(3L));
client.get().uri("/posts").exchange().expectStatus().isOk().expectBodyList(Post.class).hasSize(10);
client.get()
.uri(uriBuilder -> uriBuilder.path("/posts").queryParam("page", 1).queryParam("q", "test").build())
.exchange().expectStatus().isOk().expectBodyList(Post.class).hasSize(5);
client.get().uri("/posts/count").exchange().expectStatus().isOk().expectBody().jsonPath("$.count")
.isEqualTo(15);
client.get().uri(uriBuilder -> uriBuilder.path("/posts/count").queryParam("q", "5").build()).exchange()
.expectStatus().isOk().expectBody().jsonPath("$.count").isEqualTo(3);
verify(posts, times(1)).findAll(any(Sort.class));
verify(posts, times(1)).findByTitleContains(anyString(), any(Pageable.class));
verify(posts, times(1)).count();
verify(posts, times(1)).countByTitleContains(anyString());
verifyNoMoreInteractions(posts);
}
}
When running the tests, it will complain there are failures in the verify
clause.
[ERROR] Failures:
[ERROR] PostControllerTest$CreatingPost.shouldReturn201WhenCreatingPost:280
com.example.demo.repository.PostRepository#0 bean.save(
<any com.example.demo.domain.Post>
);
Wanted 1 time:
-> at com.example.demo.PostControllerTest$CreatingPost.shouldReturn201WhenCreatingPost(PostControllerTest.java:280)
But was 3 times:
-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
[ERROR] PostControllerTest$CreatingPost.shouldReturn422WhenCreatingPostWithInvalidBody:251
No interactions wanted here:
-> at com.example.demo.PostControllerTest$CreatingPost.shouldReturn422WhenCreatingPostWithInvalidBody(PostControllerTest.java:251)
But found these interactions on mock 'com.example.demo.repository.PostRepository#0 bean':
-> at com.example.demo.web.PostController.delete(PostController.java:107)
-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
-> at com.example.demo.web.PostController.updateStatus(PostController.java:92)
-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
-> at com.example.demo.web.PostController.update(PostController.java:77)
-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
***
For your reference, here is the list of all invocations ([?] - means unverified).
1. -> at com.example.demo.web.PostController.delete(PostController.java:107)
2. -> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
3. [?]-> at com.example.demo.web.PostController.updateStatus(PostController.java:92)
4. [?]-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
5. [?]-> at com.example.demo.web.PostController.update(PostController.java:77)
6. [?]-> at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118)
All the mocks are not reset as expected when using @Nested
.
Updated: The complete codes are hosted on Github. Add reset
manually in the afterEache
hook to overcome this issue, see here.