これは、なにをしたくて書いたもの?
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を差し替えて、テストの終了時に
戻すようなことをするのが良さそうな気がします。