Mockito does not reset when using @Nested class in Spring boot tests
Asked Answered
H

0

6

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.

Holstein answered 5/7, 2020 at 6:8 Comment(2)
Where is this method located? PostControllerTest$CreatingPost.shouldReturn422WhenCreatingPostWithInvalidBody:251Surety
Spring testing does not work with nested classes.. See e.g. github.com/spring-projects/spring-framework/issues/19930Rumsey

© 2022 - 2024 — McMap. All rights reserved.