CLOVER🍀

That was when it all began.

Spring TestとMockito(MockBean)を合わせて使った時の挙動を確認する

これは、なにをしたくて書いたもの?

Spring Bootのテスト機能を使うと、Mockitoと簡単に組み合わせられるようになります。

こちらを使った時の動作を、ちゃんと見ておきたいなと思いまして。

Spring BootとMockito

Spring BootのMockitoに関するドキュメントは、こちら。

Core Features / Testing / Testing Spring Boot Applications / Mocking and Spying Beans

関連するクラスのJavadocはこちら。

org.springframework.boot.test.mock.mockito (Spring Boot 3.1.5 API)

どういうものかというと、@MockBean(または@SpyBean)を使うことでApplicationContext内のBeanをMockitoのモックに
置き換えることができます。

Spring Boot includes a @MockBean annotation that can be used to define a Mockito mock for a bean inside your ApplicationContext. You can use the annotation to add new beans or replace a single existing bean definition.

これらのアノテーションは、以下の箇所で使用できます。

  • テストクラスや、テストクラス内のフィールド
  • @Configurationクラスやそのフィールド

作成したモックは、対象のBeanを使用しているクラスにインジェクションされます。つまり、依存するBeanがモックに置き換えられます。

When used on a field, the instance of the created mock is also injected.

そして、モックはテストメソッドの終了後に自動的にリセットされます。

Mock beans are automatically reset after each test method.

便利ですが注意点もあり、ドキュメントでは@MockBeanや@SpyBeanの利用はキャッシュキーに影響を与え、ApplicationContextの
再利用ができなくなる可能性を示唆しています。

While Spring’s test framework caches application contexts between tests and reuses a context for tests sharing the same configuration, the use of @MockBean or @SpyBean influences the cache key, which will most likely increase the number of contexts.

これは、Springのコンテナの作成が必要になり、テストの実行時間が伸びることになります。

今回は、このあたりの挙動を見てみようと思います。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.8.1 2023-08-24
OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.4 (dfbb324ad4a7c8fb0bf182e6d91b0ae20e3d2dd9)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.8.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-87-generic", arch: "amd64", family: "unix"

お題

6つのSpringのBeanを作成し、以下のような同じBeanを使うような定義を含む構成にします。

classDiagram
    FooMessageService --> MessageService: テスト時にモックにして使う
    BarMessageService --> MessageService: テスト時もそのまま使う
    FooSearchService --> SearchService: テスト時にモックにして使う
    BarSearchService --> SearchService: テスト時もそのまま使う
    class FooMessageService {
      +getMessage()
    }
    class BarMessageService {
      +getMessage()
    }
    class MessageService {
      +get()
    }
    class FooSearchService {
      +find()
    }
    class BarSearchService {
      +find()
    }
    class SearchService {
      +find()
    }

この時、注釈のようにテスト時に依存先のBeanをモックにする/しないを切り替えて挙動を確認したいと思います。
テストは、共有するBeanを「使う側」のみ作成します。

Spring Bootプロジェクトを作成する

それでは、まずはSpring Bootプロジェクトを作成します。今回は依存関係は不要なので、以下の指定で作成。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=3.1.5 \
  -d javaVersion=17 \
  -d type=maven-project \
  -d name=mock-bean-example \
  -d groupId=org.littlewings \
  -d artifactId=mock-bean-example \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.test \
  -d baseDir=mock-bean-example | tar zxvf -

展開されたディレクトリ内へ移動。

$ cd mock-bean-example

Maven依存関係等は、このようになっています。

        <properties>
                <java.version>17</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>

生成されたクラスは削除。

$ rm src/main/java/org/littlewings/spring/test/MockBeanExampleApplication.java src/test/java/org/littlewings/spring/test/MockBeanExampleApplicationTests.java

ソースコードを作成する

ソースコードを作成していきます。

共有するBean、その1。

src/main/java/org/littlewings/spring/test/service/MessageService.java

package org.littlewings.spring.test.service;

import org.springframework.stereotype.Service;

@Service
public class MessageService {
    public String get() {
        return "Hello World!!";
    }
}

使う側のBean。

src/main/java/org/littlewings/spring/test/service/FooMessageService.java

package org.littlewings.spring.test.service;

import org.springframework.stereotype.Service;

@Service
public class FooMessageService {
    private MessageService messageService;

    public FooMessageService(MessageService messageService) {
        this.messageService = messageService;
    }

    public String getMessage() {
        return messageService.get();
    }
}

src/main/java/org/littlewings/spring/test/service/BarMessageService.java

package org.littlewings.spring.test.service;

import org.springframework.stereotype.Service;

@Service
public class BarMessageService {
    private MessageService messageService;

    public BarMessageService(MessageService messageService) {
        this.messageService = messageService;
    }

    public String getMessage() {
        return messageService.get();
    }
}

共有するBean、その2。

src/main/java/org/littlewings/spring/test/service/SearchService.java

package org.littlewings.spring.test.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SearchService {
    public List<String> find() {
        return List.of("Java", "Spring");
    }
}

使う側のBean。

src/main/java/org/littlewings/spring/test/service/FooSearchService.java

package org.littlewings.spring.test.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class FooSearchService {
    private SearchService searchService;

    public FooSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    public List<String> find() {
        return searchService.find();
    }
}

src/main/java/org/littlewings/spring/test/service/BarSearchService.java

package org.littlewings.spring.test.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BarSearchService {
    private SearchService searchService;

    public BarSearchService(SearchService searchService) {
        this.searchService = searchService;
    }

    public List<String> find() {
        return searchService.find();
    }
}

mainメソッドを持ったクラスも用意しておきます。

src/main/java/org/littlewings/spring/test/App.java

package org.littlewings.spring.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

今回は、テストの構成に使われるだけですが。

テストを書いて確認する

それでは、テストを書いて確認していきます。

今回はテストの実行順(クラスを含む)を固定したいので、以下の設定を入れておきます。

src/test/resources/junit-platform.properties

junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation

確認は、mvn testで実施していきます。

モックがテストごとにリセットされることを確認する

まずはこんなテストを用意。

src/test/java/org/littlewings/spring/test/service/FooMessageServiceTest.java

package org.littlewings.spring.test.service;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.MockReset;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@SpringBootTest
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Order(1)
class FooMessageServiceTest {
    @Autowired
    private FooMessageService fooMessageService;

    @MockBean
    private MessageService messageService;

    @Test
    @Order(1)
    void test1() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        when(messageService.get()).thenReturn("Hello Mock!!");

        assertThat(fooMessageService.getMessage()).isEqualTo("Hello Mock!!");

        verify(messageService, times(1)).get();
    }

    @Test
    @Order(2)
    void test2() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        assertThat(fooMessageService.getMessage()).isNull();
    }
}

依存するBeanをモックにします。

    @MockBean
    private MessageService messageService;

テストは、ともにモックであることを確認してから、挙動を見ていきます。モックのセットアップは片方だけ行っておき、テストの
実行順はモックのセットアップを行う方が先になるようにしています。

    @Test
    @Order(1)
    void test1() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        when(messageService.get()).thenReturn("Hello Mock!!");

        assertThat(fooMessageService.getMessage()).isEqualTo("Hello Mock!!");

        verify(messageService, times(1)).get();
    }

    @Test
    @Order(2)
    void test2() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        assertThat(fooMessageService.getMessage()).isNull();
    }

テストを実行。

$ mvn test -Dtest=FooMessageServiceTest

このテストはパスします。

ではここで、@MockBeanの設定を以下のように変更します。

    @MockBean(reset = MockReset.NONE)
    private MessageService messageService;

すると、テストが失敗するようになります。

[ERROR] Failures:
[ERROR]   FooMessageServiceTest.test2:40
expected: null
 but was: "Hello Mock!!"

これは、ドキュメントにあった以下の挙動を打ち消したものです。つまり、モックがリセットされなくなり別のテストに影響を与えるように
なりました。

Mock beans are automatically reset after each test method.

つまり、なにもモックのセットアップの状態をしていない以下のテストが、先に実行されてモックのセットアップを行った別のテストの
内容を見ていることになります。

    @Test
    @Order(2)
    void test2() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        assertThat(fooMessageService.getMessage()).isNull();
    }

デフォルトだと、以下の指定と同じということになりますね。

    @MockBean(reset = MockReset.AFTER)
    private MessageService messageService;
モックを使わないテストを追加して、Springのコンテキストの構築の様子を見てみる

次は、モックを使わないテストを書いてみます。

src/test/java/org/littlewings/spring/test/service/BarMessageServiceTest.java

package org.littlewings.spring.test.service;

import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.util.ReflectionTestUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mockingDetails;

@SpringBootTest
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
@Order(2)
class BarMessageServiceTest {
    @Autowired
    private BarMessageService barMessageService;

    @Test
    void test() {
        Object messageService = ReflectionTestUtils.getField(barMessageService, "messageService");
        assertThat(mockingDetails(messageService).isMock()).isFalse();

        assertThat(barMessageService.getMessage()).isEqualTo("Hello World!!");
    }
}

使用しているBeanが依存しているのは、先ほど別のテストでモックにしたBeanですが、こちらがモックになっていないことを確認します。

    @Test
    void test() {
        Object messageService = ReflectionTestUtils.getField(barMessageService, "messageService");
        assertThat(mockingDetails(messageService).isMock()).isFalse();

        assertThat(barMessageService.getMessage()).isEqualTo("Hello World!!");
    }

まずは単体で実行。

$ mvn test -Dtest=BarMessageServiceTest

こちらは問題なくパスします。

次に、両方とも実行。

$ mvn test -Dtest=*MessageServiceTest

こちらも問題なくパスしますが、以下のようにSpringのコンテキストの構築がそれぞれで行われてしまいます。

16:39:08.224 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageServiceTest]: FooMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
16:39:08.307 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:39:08.667+09:00  INFO 18914 --- [           main] o.l.s.t.service.FooMessageServiceTest    : Starting FooMessageServiceTest using Java 17.0.8.1 with PID 18914 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T16:39:10.338+09:00  INFO 18914 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.BarMessageServiceTest]: BarMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T16:39:10.343+09:00  INFO 18914 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.BarMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:39:10.405+09:00  INFO 18914 --- [           main] o.l.s.t.service.BarMessageServiceTest    : Starting BarMessageServiceTest using Java 17.0.8.1 with PID 18914 (started by xxxxx in /path/to/mock-bean-example)

〜省略〜

これは、ドキュメントに書かれていたように、コンテキストの構成情報が異なるため再利用ができなくなったことを指しています。

While Spring’s test framework caches application contexts between tests and reuses a context for tests sharing the same configuration, the use of @MockBean or @SpyBean influences the cache key, which will most likely increase the number of contexts.

他のグルーピングのテストも追加して、コンテキストの構築の様子を見てみる

同じBeanを共有する構成としたコードは、もう1組ありました。こちらのテストコードも追加してみます。

src/test/java/org/littlewings/spring/test/service/FooSearchServiceTest.java

package org.littlewings.spring.test.service;

import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@SpringBootTest
@Order(3)
class FooSearchServiceTest {
    @Autowired
    private FooSearchService fooSearchService;

    @MockBean
    private SearchService searchService;

    @Order(1)
    void test1() {
        assertThat(mockingDetails(searchService).isMock()).isTrue();

        when(searchService.find()).thenReturn(List.of("Mockito"));

        assertThat(fooSearchService.find()).isEqualTo(List.of("Mockito"));

        verify(searchService, times(1)).find();
    }

    @Test
    @Order(2)
    void test2() {
        assertThat(mockingDetails(searchService).isMock()).isTrue();

        assertThat(searchService.find()).isEmpty();
    }
}

src/test/java/org/littlewings/spring/test/service/BarSearchServiceTest.java

package org.littlewings.spring.test.service;

import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mockingDetails;

@SpringBootTest
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
@Order(4)
class BarSearchServiceTest {
    @Autowired
    private BarSearchService barSearchService;

    @Test
    void test1() {
        Object searchService = ReflectionTestUtils.getField(barSearchService, "searchService");
        assertThat(mockingDetails(searchService).isMock()).isFalse();

        assertThat(barSearchService.find()).isEqualTo(List.of("Java", "Spring"));
    }
}

テストとして実装している内容こそ異なりますが、確認しているポイントは先ほどのものと同じです。

というわけで、テストを実行してみます。

$ mvn test

テストは問題なくパスしますが、Springのコンテキストの構築は3回行われました。

16:45:35.204 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageServiceTest]: FooMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
16:45:35.290 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:45:35.623+09:00  INFO 19254 --- [           main] o.l.s.t.service.FooMessageServiceTest    : Starting FooMessageServiceTest using Java 17.0.8.1 with PID 19254 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T16:45:37.138+09:00  INFO 19254 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.BarMessageServiceTest]: BarMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T16:45:37.142+09:00  INFO 19254 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.BarMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:45:37.203+09:00  INFO 19254 --- [           main] o.l.s.t.service.BarMessageServiceTest    : Starting BarMessageServiceTest using Java 17.0.8.1 with PID 19254 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T16:45:37.345+09:00  INFO 19254 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooSearchServiceTest]: FooSearchServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T16:45:37.349+09:00  INFO 19254 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooSearchServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:45:37.388+09:00  INFO 19254 --- [           main] o.l.s.test.service.FooSearchServiceTest  : Starting FooSearchServiceTest using Java 17.0.8.1 with PID 19254 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T16:45:37.556+09:00  INFO 19254 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.BarSearchServiceTest]: BarSearchServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T16:45:37.558+09:00  INFO 19254 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.BarSearchServiceTest

4回ではないのは、モックを使っていないBarMessageServiceTestとBarSearchServiceTestではキャッシュしたコンテキストが使い回せる
からですね。

ログをよく見ると、こういう内容はいつも出ていますが

16:45:35.204 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageServiceTest]: FooMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
16:45:35.290 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageServiceTest

コンテキストを作り直すことになった時は、以下のログが出力されるんですね。

2023-10-22T16:45:37.203+09:00  INFO 19254 --- [           main] o.l.s.t.service.BarMessageServiceTest    : Starting BarMessageServiceTest using Java 17.0.8.1 with PID 19254 (started by xxxxx in /path/to/mock-bean-example)

後続で起動したBarSearchServiceTestには、このログがありませんでした。

では、モックを使ったテストのみではどうでしょうか。

$ mvn test -Dtest=Foo*ServiceTest

こちらもキーが異なるので、2回コンテキストが構築されます。

16:49:29.975 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageServiceTest]: FooMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
16:49:30.064 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:49:30.369+09:00  INFO 19387 --- [           main] o.l.s.t.service.FooMessageServiceTest    : Starting FooMessageServiceTest using Java 17.0.8.1 with PID 19387 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T16:49:31.727+09:00  INFO 19387 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooSearchServiceTest]: FooSearchServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T16:49:31.731+09:00  INFO 19387 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooSearchServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:49:31.783+09:00  INFO 19387 --- [           main] o.l.s.test.service.FooSearchServiceTest  : Starting FooSearchServiceTest using Java 17.0.8.1 with PID 19387 (started by xxxxx in /path/to/mock-bean-example)
テスト対象は同じで、モックを使わないテストを追加する

ここまでで、なんとなく挙動は確認できた気がします。

それでは「テスト対象は同じで、モックは使わないパターン」のテストを追加してみましょう。結果はわかる気はしますが。

こういうものですね。

src/test/java/org/littlewings/spring/test/service/FooMessageService2Test.java

package org.littlewings.spring.test.service;

import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.util.ReflectionTestUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mockingDetails;

@SpringBootTest
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
@Order(5)
public class FooMessageService2Test {
    @Autowired
    private FooMessageService fooMessageService;

    @Test
    void test1() {
        Object messageService = ReflectionTestUtils.getField(fooMessageService, "messageService");
        assertThat(mockingDetails(messageService).isMock()).isFalse();

        assertThat(fooMessageService.getMessage()).isEqualTo("Hello World!!");
    }
}

実行。

$ mvn test -Dtest=FooMessageService*Test

予想できる挙動ですが、コンテキストが2回構築されます。

16:55:55.763 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageServiceTest]: FooMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
16:55:55.862 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:55:56.185+09:00  INFO 19782 --- [           main] o.l.s.t.service.FooMessageServiceTest    : Starting FooMessageServiceTest using Java 17.0.8.1 with PID 19782 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T16:55:57.792+09:00  INFO 19782 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageService2Test]: FooMessageService2Test does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T16:55:57.798+09:00  INFO 19782 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageService2Test

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T16:55:57.843+09:00  INFO 19782 --- [           main] o.l.s.t.service.FooMessageService2Test   : Starting FooMessageService2Test using Java 17.0.8.1 with PID 19782 (started by xxxxx in /path/to/mock-bean-example)

全部のテストを実行した場合は、モックを使わないテストはそれぞれでコンテキストが共有できるので、コンテキスの構築は3回になります。

$ mvn test
モック対象も同じテストを追加する

最後にこの状態で、モック対象も同じテストをもうひとつ追加します。FooMessageServiceTestのコピーですね。

src/test/java/org/littlewings/spring/test/service/FooMessageService3Test.java

package org.littlewings.spring.test.service;

import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;

@SpringBootTest
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Order(6)
class FooMessageService3Test {
    @Autowired
    private FooMessageService fooMessageService;

    @MockBean
    private MessageService messageService;

    @Test
    @Order(1)
    void test1() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        when(messageService.get()).thenReturn("Hello Mock!!");

        assertThat(fooMessageService.getMessage()).isEqualTo("Hello Mock!!");

        verify(messageService, times(1)).get();
    }

    @Test
    @Order(2)
    void test2() {
        assertThat(mockingDetails(messageService).isMock()).isTrue();

        assertThat(fooMessageService.getMessage()).isNull();
    }
}

これはどうなるかというと

$ mvn test -Dtest=FooMessageService*Test

先ほどモックを使わないテストを追加したので、コンテキストの構築回数は2回になっていますが、モックも含めて定義が同じであれば
コンテキストが使い回される(FooMessageServiceTestとFooMessageService3Testで定義が同じなので、同じコンテキストが再利用される)
ことが確認できます。

ing.test.service.FooMessageServiceTest]: FooMessageServiceTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
17:08:26.641 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageServiceTest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T17:08:27.049+09:00  INFO 20970 --- [           main] o.l.s.t.service.FooMessageServiceTest    : Starting FooMessageServiceTest using Java 17.0.8.1 with PID 20970 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T17:08:29.538+09:00  INFO 20970 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageService2Test]: FooMessageService2Test does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T17:08:29.546+09:00  INFO 20970 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageService2Test

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-10-22T17:08:29.644+09:00  INFO 20970 --- [           main] o.l.s.t.service.FooMessageService2Test   : Starting FooMessageService2Test using Java 17.0.8.1 with PID 20970 (started by xxxxx in /path/to/mock-bean-example)


〜省略〜


2023-10-22T17:08:29.948+09:00  INFO 20970 --- [           main] t.c.s.AnnotationConfigContextLoaderUtils : Could not detect default configuration classes for test class [org.littlewings.spring.test.service.FooMessageService3Test]: FooMessageService3Test does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
2023-10-22T17:08:29.956+09:00  INFO 20970 --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Found @SpringBootConfiguration org.littlewings.spring.test.App for test class org.littlewings.spring.test.service.FooMessageService3Test

こんなところでしょうか。

おわりに

Spring TestとMockito(@MockBean)を組み合わせた時に、どのような動作になるのか確認してみました。
※@SpyBeanはパスしていますが

@MockBeanはSpring BootでMockitoを使う時には便利ですが、こういう挙動になるというのも押さえておかないといけない気がしますね。

テストの速度が問題になってくるような時は、@MockBeanに頼らずに自分でモックとして作成したBeanを差し替えて、テストの終了時に
戻すようなことをするのが良さそうな気がします。