Spring Boot/Spring Frameworkのテストに関するドキュメントをざっくり眺めてみる - CLOVER🍀
参照するドキュメントとSpring Web
MVC
のテスト
今回、Spring Web
MVC
のテストを見ていくにあたり、参照するドキュメントは主に次の2つです。
Spring Bootのテストに関するドキュメント。
Core Features / Testing
さらに絞り込むと、Spring Boot Applicationのテストに関するセクションですね。
Core Features / Testing / Testing Spring Boot Applications
Spring Framework
のテストに関するドキュメント。
Testing
こちらも絞り込むと、インテグレーションテストに関するセクションになります。
Testing / Integration Testing
基本的には、Spring Bootのドキュメントを見ていき、テスト自体を書く時に使うクラスに関しては
Spring Framework
のドキュメントを
必要に応じて見ていく、という感じになります。
Spring Web
MVC
のテストをする方法ですが、Spring Bootや
Spring Framework
のテストに関するドキュメントを見ていくと
複数あることに気づきます。
読み進め方ですが、まずはSpring Bootプロジェクトを作成してサンプルプログラムを作り、そこからテストのバリエーションを変えつつ
テストコードを書いていくことにします。
順番は、以下にします。
@WebMvcTest
を使ったテスト
Core Features / Testing / Testing Spring Boot Applications / Auto-configured Spring MVC Tests
モック環境でのテスト
Core Features / Testing / Testing Spring Boot Applications / Testing with a mock environment
組み込みサーバーを起動してのテスト
Core Features / Testing / Testing Spring Boot Applications / Testing with a running server
組み込み
Tomcat
を起動することになります。
このあたりを、適宜ドキュメントの内容を絡めながら書いていきたいと思います。
HtmlUnit
や
Selenium
を使ったテストは扱いません。
ちなみに、
Spring Framework
のテストのセクションには、
HtmlUnit
やWebDriver、
Geb
とMockMvcを合わせて使う方法について書かれたり
しています。興味があれば、こちらをどうぞ。
Testing / Integration / MockMvc / Testing HtmlUnit Integration
では、進めていきます。
今回の環境は、こちら。
$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)
$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-107-generic", arch: "amd64", family: "unix"
まずはSpring Bootプロジェクトを作成します。
$ curl -s https://start.spring.io/starter.tgz \
-d bootVersion=2.6.6 \
-d javaVersion=17 \
-d name=webmvc-testing \
-d groupId=org.littlewings \
-d artifactId=webmvc-testing \
-d version=0.0.1-SNAPSHOT \
-d packageName=org.littlewings \
-d dependencies=web,webflux \
-d baseDir=webmvc-testing | tar zxvf -
依存関係に
web
以外に
webflux
が入っていますが、この理由は後で書きます。
プロジェクト内へ移動。
$ cd webmvc-testing
ディレクト
リ構成。
$ tree
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── org
│ │ └── littlewings
│ │ └── WebmvcTestingApplication.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── org
└── littlewings
└── WebmvcTestingApplicationTests.java
12 directories, 7 files
Maven
での依存関係および
プラグイン
設定。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
含まれているソースコードは、こんな感じですね。
src/main/java/org/littlewings/WebmvcTestingApplication.java
package org.littlewings;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebmvcTestingApplication {
public static void main(String[] args) {
SpringApplication.run(WebmvcTestingApplication.class, args);
src/test/java/org/littlewings/WebmvcTestingApplicationTests.java
package org.littlewings;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class WebmvcTestingApplicationTests {
@Test
void contextLoads() {
テストコードには、@SpringBootTest
アノテーションが付与されています。
このあたりは、いったん削除します。
$ rm src/main/java/org/littlewings/WebmvcTestingApplication.java src/test/java/org/littlewings/WebmvcTestingApplicationTests.java
@SpringBootTestアノテーションについて
@SpringBootTest
アノテーションについての説明を少し見てみます。
@SpringBootTest
アノテーションは、Spring Bootの機能が必要な場合に@ContextConfiguration
アノテーションの代わりに使用します。
Spring Boot provides a @SpringBootTest annotation, which can be used as an alternative to the standard spring-test @ContextConfiguration annotation when you need Spring Boot features. The annotation works by creating the ApplicationContext used in your tests through SpringApplication. In addition to @SpringBootTest a number of other annotations are also provided for testing more specific slices of an application.
Core Features / Testing / Testing Spring Boot Applications
SpringApplication
を使い、ApplicationContext
の作成を行います。また、テストでの機能が不足する場合、Auto Configurationを行うための
機能も提供しています(スライスと読んでいます)。
Core Features / Testing / Testing Spring Boot Applications / https://docs.spring.io/spring-boot/docs/2.6.6/reference/html/features.html#features.testing.spring-boot-applications.autoconfigured-tests
そして、Web環境の方に目を向けてみると、@SpringBootTest
アノテーションはサーバーを開始しないと書いています。
ですが、webEnvironment
属性を使ってテストの実行方法を決められるようです。
By default, @SpringBootTest will not start a server. You can use the webEnvironment attribute of @SpringBootTest to further refine how your tests run:
Core Features / Testing / Testing Spring Boot Applications
@SpringBootTest
アノテーションのwebEnvironment
属性には、以下の4種類が指定できます。
デフォルトの挙動で、Web用のApplicationContext
をロードして、モックWeb環境を提供する
組み込みサーバーは起動しない
@AutoConfigureMockMvc
アノテーションまたは@AutoConfigureWebTestClient
アノテーションと組み合わせることで、Webアプリケーションのモックベースのテストが可能になる
RANDOM_PORT
WebServerApplicationContext
をロードして、本物のWeb環境を提供する
組み込みサーバーが起動し、ランダムなポートでリッスンする
DEFINED_PORT
WebServerApplicationContext
をロードして、本物のWeb環境を提供する
組み込みサーバーが起動し、application.properties
で定義されたポートまたはデフォルトの8080
ポートでリッスンする
SpringApplication
によってApplicationContext
はロードするものの、モックやそれ以外のいずれのWeb環境も提供しない
テストをモックWeb環境で行うか、組み込みサーバーで行うか、という感じになります(まったくWeb環境を使わないという選択肢もありますが)。
Spring Web MVCが使用可能な場合は、Spring Web MVCベースのApplicationContext
が構成されるようです。
If Spring MVC is available, a regular MVC-based application context is configured.
Core Features / Testing / Testing Spring Boot Applications / Detecting Web Application Type
サンプルアプリケーションを作成する
テスト対象のサンプルアプリケーションを作成しましょう。お題は、ちょっとしたechoプログラムにします。
mainクラス。
src/main/java/org/littlewings/spring/webmvc/App.java
package org.littlewings.spring.webmvc;
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);
RestController。GETとPOST用のメソッドを用意して、POSTの場合はJSONでリクエストを受け取るようにします。
src/main/java/org/littlewings/spring/webmvc/EchoController.java
package org.littlewings.spring.webmvc;
import java.util.Optional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("echo")
public class EchoController {
MessageDecorationService messageDecorationService;
public EchoController(MessageDecorationService messageDecorationService) {
this.messageDecorationService = messageDecorationService;
@GetMapping("get")
public EchoResponse get(@RequestParam String message) {
Thread.dumpStack();
return EchoResponse
.create(
messageDecorationService
.decorate(Optional.ofNullable(message).orElse("Hello World[get]!!")),
Thread.currentThread().getName()
@PostMapping("post")
public EchoResponse post(@RequestBody EchoRequest echoRequest) {
Thread.dumpStack();
return EchoResponse
.create(
messageDecorationService
.decorate(Optional.ofNullable(echoRequest.getMessage()).orElse("Hello World[post]!!")),
Thread.currentThread().getName()
レスポンスには受け取ったメッセージをServiceで加工して返し、この時に動作しているスレッド名を返すようにします。
それから、RestControllerのメソッド呼び出し時にはスタックトレースを出力するようにしています。
リクエスト用のクラス。
src/main/java/org/littlewings/spring/webmvc/EchoRequest.java
package org.littlewings.spring.webmvc;
public class EchoRequest {
String message;
public static EchoRequest create(String message) {
EchoRequest echoRequest = new EchoRequest();
echoRequest.setMessage(message);
return echoRequest;
レスポンス用のクラス。
src/main/java/org/littlewings/spring/webmvc/EchoResponse.java
package org.littlewings.spring.webmvc;
public class EchoResponse {
String message;
String threadName;
public static EchoResponse create(String message, String threadName) {
EchoResponse echoResponse = new EchoResponse();
echoResponse.setMessage(message);
echoResponse.setThreadName(threadName);
return echoResponse;
RestControllerが使うServiceクラス。
src/main/java/org/littlewings/spring/webmvc/MessageDecorationService.java
package org.littlewings.spring.webmvc;
import org.springframework.stereotype.Service;
@Service
public class MessageDecorationService {
public String decorate(String message) {
return String.format("★★★ %s !!★★★", message);
動作確認。
$ mvn spring-boot:run
OKですね。
$ curl localhost:8080/echo/get?message=Hello
{"message":"★★★ Hello !!★★★","threadName":"http-nio-8080-exec-1"}
$ curl -H 'Content-Type: application/json' localhost:8080/echo/post -d '{"message": "Hello"}'
{"message":"★★★ Hello !!★★★","threadName":"http-nio-8080-exec-2"}
では、こちらに対してテストを書いていきましょう。なお、依存関係にspring-boot-starter-webflux
が含まれていましたが、特に明示的に
説明しない限りはspring-boot-starter-web
とspring-boot-starter-test
が依存関係に含まれていればテストは動作します。
spring-boot-starter-webflux
については、必要な部分で説明します。
デフォルトの@SpringBootTestで組み込みサーバーが起動しないことを確認する
実際のテストに行く前に、@SpringBootTest
アノテーションだけでは組み込みサーバーが起動しないことを確認しておきます。
こんなテストで確認。
src/test/java/org/littlewings/spring/webmvc/SpringBootTestDefaultTest.java
package org.littlewings.spring.webmvc;
import java.io.IOException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest
public class SpringBootTestDefaultTest {
@Test
public void test() throws IOException {
HttpURLConnection conn = (HttpURLConnection) URI.create("http://localhost:8080").toURL().openConnection();
assertThatThrownBy(() -> conn.connect())
.isInstanceOf(ConnectException.class)
.hasMessage("接続を拒否されました");
ドキュメントどおりサーバーが起動していないので、接続が拒否されます。
では、先に進めていきましょう。
@WebMvcTestを使ってテストを書く
ドキュメントの順番とはだいぶ異なりますが、最初に@WebMvcTest
アノテーションを使ったテストを書くことにします。
Core Features / Testing / Testing Spring Boot Applications / Auto-configured Spring MVC Tests
こちらは、Spring Web MVCを使って実装した、特定のControllerが動作するかをテストします。
@WebMvcTest
アノテーションを使用すると、Spring Web MVCを構成するとともに限られたBeanをスキャンします。
@Controller
@ControllerAdvice
@JsonComponent
Converter
GenericConverter
Filter
HandlerInterceptor
WebMvcConfigurer
WebMvcRegistrations
HandlerMethodArgumentResolver
通常の@Component
や@ConfigurationProperties
といったBeanは、@WebMvcTest
アノテーションを使用してもスキャンされません。
使用したい場合は、@EnableConfigurationProperties
アノテーションを付与することになります。
なお、@WebMvcTest
アノテーションを使用した時に有効になるAuto Configurationは、以下にまとめられています。
Test Auto-configuration Annotations
@WebMvcTest
アノテーションを使用して作成したテストは、こちら。
src/test/java/org/littlewings/spring/webmvc/WebMvcTestTest.java
package org.littlewings.spring.webmvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(EchoController.class)
public class WebMvcTestTest {
@Autowired
MockMvc mvc;
@MockBean
MessageDecorationService messageDecorationService;
@Autowired
ObjectMapper objectMapper;
@Test
public void getTest() throws Exception {
given(messageDecorationService.decorate("WebMvcTest[get]"))
.willReturn("*** WebMvcTest[get] !!***");
.perform(
get("/echo/get")
.queryParam("message", "WebMvcTest[get]")
.andExpect(status().isOk())
.andExpect(
content()
.json(
objectMapper.writeValueAsString(
EchoResponse.create("*** WebMvcTest[get] !!***", "main")
.perform(
get("/echo/get?message=WebMvcTest[get]")
.andExpect(status().isOk())
.andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[get] !!***")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
@Test
public void postTest() throws Exception {
given(messageDecorationService.decorate("WebMvcTest[post]"))
.willReturn("*** WebMvcTest[post] !!***");
.perform(
post("/echo/post")
.contentType(MediaType.APPLICATION_JSON)
.content(
objectMapper.writeValueAsString(
EchoRequest.create("WebMvcTest[post]")
.andExpect(status().isOk())
.andExpect(header().string("content-type", MediaType.APPLICATION_JSON_VALUE))
.andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[post] !!***")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
テストクラスに@WebMvcTest
アノテーションを付与します。
@WebMvcTest(EchoController.class)
public class WebMvcTestTest {
ドキュメントに習ってControllerを@WebMvcTest
アノテーションに指定していますが、実は指定しなくても動きます。
スキャン対象に@Controller
が入っているからですね。
Controllerが依存するBeanは、モックにします。
@MockBean
MessageDecorationService messageDecorationService;
というか、最初に書いたようにスキャンされるBeanが限定されているので、今回のように別のBeanに依存しているControllerはモックで
依存関係をなんとかするなりBeanを登録するなりしないと動作しません。
もし今回のソースコードからモックに関するコードを削除すると、作成したRestControllerの依存関係が解決できずにテストに失敗します。
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'echoController' defined in file [/path/to/webmvc-testing/target/classes/org/littlewings/spring/webmvc/EchoController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.littlewings.spring.webmvc.MessageDecorationService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:740)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:415)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:303)
at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:136)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)
... 72 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.littlewings.spring.webmvc.MessageDecorationService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1799)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1355)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
... 90 more
テストにはMockMvc
を使用します。
@Autowired
MockMvc mvc;
MockMvc
自体については、こちらを参照。
Testing / Integration Testing / MockMvc
今回は、最初に依存するServiceのモックを設定して、それからMockMvc
を使ってテストしています。
@Test
public void getTest() throws Exception {
given(messageDecorationService.decorate("WebMvcTest[get]"))
.willReturn("*** WebMvcTest[get] !!***");
.perform(
get("/echo/get")
.queryParam("message", "WebMvcTest[get]")
.andExpect(status().isOk())
.andExpect(
content()
.json(
objectMapper.writeValueAsString(
EchoResponse.create("*** WebMvcTest[get] !!***", "main")
.perform(
get("/echo/get?message=WebMvcTest[get]")
.andExpect(status().isOk())
.andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[get] !!***")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
今回は、なんとなくQueryStringをqueryParam
で設定したり、直接URLに付けたりしてみました。また、JSONレスポンスのアサーション方法も
分けています。
アサーション結果を見るとわかるのですが、今回のテストではRestControllerはmainスレッドで動作していますね。
.andExpect(jsonPath("message", Matchers.is("*** WebMvcTest[get] !!***")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
RestControllerのメソッド呼び出し時のスタックトレースはこちら。
java.lang.Exception: Stack trace
at java.base/java.lang.Thread.dumpStack(Thread.java:1380)
at org.littlewings.spring.webmvc.EchoController.get(EchoController.java:23)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:199)
at org.littlewings.spring.webmvc.WebMvcTestTest.getTest(WebMvcTestTest.java:35)
この少し後に書くモック環境でのテストについて書いた内容を見るとわかりますが、@WebMvcTest
アノテーションを使った時もモック環境で
動作していることになります。
MockMvcについて
MockMvc
について、もう少し深堀してみましょう。
MockMvc
にはスタンドアロンモードとSpringのConfigurationを使ったモードの2つのセットアップ方法があります。
Testing / Integration Testing / MockMvc / Setup Choices
Spring Bootでは、SpringのConfigurationを使ったモードでセットアップするようです。
https://github.com/spring-projects/spring-boot/blob/v2.6.6/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java#L78-L87
https://github.com/spring-projects/spring-boot/blob/v2.6.6/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/web/servlet/MockMvcAutoConfiguration.java#L95-L99
MockMvc
のセットアップ自体は完了しているので、あとはリクエストを設定したり、レスポンスをアサーションしたりといった感じの
使い方になります。
Testing / Integration Testing / MockMvc / Performing Requests
Testing / Integration Testing / MockMvc / Defining Expectations
今回は通常のControllerで使いたくなるようなフォワード先やモデルのアサーションは行いませんが、サンプルがあるのでこちらを
参考にするとよいでしょう。
https://github.com/spring-projects/spring-framework/tree/v5.3.18/spring-test/src/test/java/org/springframework/test/web/servlet/samples
Servlet Filterを登録する機能もありそうですが
Testing / Integration Testing / MockMvc / Filter Registrations
こちらは、@WebMvcTest
アノテーションの属性で表現することになりそうです。
WebMvcTest (Spring Boot 2.6.6 API)
ドキュメントを見ると、@AutoConfigureMockMvc
アノテーションを使うとよい、とは書かれていますが。
Core Features / Testing / Testing Spring Boot Applications / Auto-configured Spring MVC Tests
また、MockMvc
とE2Eテストの比較についてもドキュメントに書かれています。
Testing / Integration Testing / MockMvc / MockMvc vs End-to-End Tests
MockMvc
はServletのモック実装に基づいて構築されていて、実際のサーブレットコンテナと比べると異なる部分もあります。
jsessoinid
Cookieがない、フォワードやリダイレクト、サーブレットの例外ハンドリングは行われず、(Spring Bootではあまり関係ないですが)
JSPのレンダリングも行われません。
※Theymeleafなどのテンプレートエンジンでのレンダリングや、JSON、XMLなどでのレスポンスは行えるようです
MockMvc
を使用したテストではControllerに依存するBeanをモック化したりして、Webレイヤーに限定したテストがしやすくなるところが
ポイントです。SpringとしてはMockMvc
の利用もインテグレーションテストに位置づけていますが、単体テストに近いものではあります。
このため、E2Eテストのようなブラックボックス的なテストと異なり、MockMvc
ではHandler(Controller)が使われたこと、例外処理で
HandlerExceptionResolver
が使われたこと、モデルの属性やバインディングエラーなどを確認できます。
このような特性を踏まえつつ、どのテストを使うかを決めていくことになるんでしょうね。
モック環境でテストする
次は、モック環境でのテストを行います。
Core Features / Testing / Testing Spring Boot Applications / Testing with a mock environment
具体的には、@SpringBootTest
アノテーションと@AutoConfigureMockMvc
アノテーションの組み合わせになります。
作成したテストコードはこちら。
src/test/java/org/littlewings/spring/webmvc/AutoConfigureMockMvcTest.java
package org.littlewings.spring.webmvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class AutoConfigureMockMvcTest {
@Test
public void getTest(@Autowired MockMvc mvc) throws Exception {
.perform(
get("/echo/get")
.queryParam("message", "MockMvc[get]")
.andExpect(status().isOk())
.andExpect(jsonPath("message", Matchers.is("★★★ MockMvc[get] !!★★★")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
@Test
public void postTest(@Autowired MockMvc mvc, @Autowired ObjectMapper objectMapper) throws Exception {
.perform(
post("/echo/post")
.contentType(MediaType.APPLICATION_JSON)
.content(
objectMapper.writeValueAsString(
EchoRequest.create("MockMvc[post]")
.andExpect(status().isOk())
.andExpect(header().string("content-type", MediaType.APPLICATION_JSON_VALUE))
.andExpect(jsonPath("message", Matchers.is("★★★ MockMvc[post] !!★★★")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
@WebMvcTest
アノテーションを使った時と同じようにMockMvc
を使ったテストになりますが、先ほどとの違いはRestControllerが
依存するBeanをモック化していないことですね。
またテストクラスに付与するアノテーションで、明示的なControllerの指定もなくなりました。
@SpringBootTest
@AutoConfigureMockMvc
public class AutoConfigureMockMvcTest {
RestControllerのメソッド呼び出し時のスタックトレースは、こちら。
java.lang.Exception: Stack trace
at java.base/java.lang.Thread.dumpStack(Thread.java:1380)
at org.littlewings.spring.webmvc.EchoController.get(EchoController.java:23)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at org.springframework.test.web.servlet.TestDispatcherServlet.service(TestDispatcherServlet.java:72)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.springframework.mock.web.MockFilterChain$ServletFilterProxy.doFilter(MockFilterChain.java:167)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:134)
at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:199)
at org.littlewings.spring.webmvc.AutoConfigureMockMvcTest.getTest(AutoConfigureMockMvcTest.java:22)
よく見ると、@WebMvcTest
アノテーションを使った時のスタックトレースとまったく同じです。
そして、スレッド名もmain
であることが確認できます。
.andExpect(jsonPath("message", Matchers.is("★★★ MockMvc[post] !!★★★")))
.andExpect(jsonPath("threadName", Matchers.is("main")));
ということは、この2つのテスト方法では、同じモック環境で動作していることになりますね。違いはBeanのスキャン範囲といったところ
でしょうか(@SpringBootTest
アノテーションを使っている差かと)。
ちなみに、このテストの中に@MockBean
アノテーションを使ったコードを追加することで、依存するBeanをモック化することは
ふつうにできます。
@MockBean
MessageDecorationService messageDecorationService;
組み込みサーバーを起動してテストする
最後は、組み込みサーバーを起動してテストを行います。
Core Features / Testing / Testing Spring Boot Applications / Testing with a running server
こちらは、@SpringBootTest
アノテーションのwebEnvironment
属性にRANDOM_PORT
またはDEFINED_PORT
を指定することで
実現できます。今回は、RANDOM_PORT
を使用します。
作成したテストはこちら。
src/test/java/org/littlewings/spring/webmvc/SpringBootTestWebEnvironmentTest.java
package org.littlewings.spring.webmvc;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "server.tomcat.threads.max=1"
public class SpringBootTestWebEnvironmentTest {
@Test
public void getTest(@Autowired WebTestClient webClient) {
webClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/echo/get")
.queryParam("message", "WebEnvironment Test[get]")
.build()
.exchange()
.expectStatus().isOk()
.expectBody(EchoResponse.class)
.consumeWith(result -> {
EchoResponse response = result.getResponseBody();
assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[get] !!★★★");
assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
@Test
public void postTest(@Autowired WebTestClient webClient) {
webClient
.post()
.uri("/echo/post")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(EchoRequest.create("WebEnvironment Test[post]"))
.exchange()
.expectStatus().isOk()
.expectBody(EchoResponse.class)
.consumeWith(result -> {
EchoResponse response = result.getResponseBody();
assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[post] !!★★★");
assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
@SpringBootTest
アノテーションのwebEnvironment
属性はRANDOM_PORT
とし、またRestControllerがスレッド名を返すので
固定になるように組み込みTomcatのスレッドプールは1にしました。
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "server.tomcat.threads.max=1"
public class SpringBootTestWebEnvironmentTest {
テストには、WebTestClient
を使うようです。
@Test
public void getTest(@Autowired WebTestClient webClient) {
webClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/echo/get")
.queryParam("message", "WebEnvironment Test[get]")
.build()
.exchange()
.expectStatus().isOk()
.expectBody(EchoResponse.class)
.consumeWith(result -> {
EchoResponse response = result.getResponseBody();
assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[get] !!★★★");
assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
このために、依存関係にspring-boot-starter-webflux
を追加しています。
Core Features / Testing / Testing Spring Boot Applications / Testing with a running server
Testing / Integration Testing / WebTestClient
ちなみに、WebTestClient
はモック環境でも利用できるようです。
Spring WebFluxを依存関係に追加したくない場合は、TestRestTemplate
を使ってテストしてもOKです。
RestController呼び出し時のスタックトレースはこちら。
java.lang.Exception: Stack trace
at java.base/java.lang.Thread.dumpStack(Thread.java:1380)
at org.littlewings.spring.webmvc.EchoController.get(EchoController.java:23)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:889)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:833)
Tomcatのスレッドプール内で動作していることがわかりますね。
また、アサーション時に確認していたスレッド名も、main
ではなくなっていました。
.consumeWith(result -> {
EchoResponse response = result.getResponseBody();
assertThat(response.getMessage()).isEqualTo("★★★ WebEnvironment Test[get] !!★★★");
assertThat(response.getThreadName()).isEqualTo("http-nio-auto-1-exec-1");
Beanのモック化も行わなず、Spring Web MVCアプリケーションとして完全な形で動作します。E2Eテストといった用途のイメージで
使うことになるんでしょうね。
これでひととおりSpring Web MVCのテスト方法の確認はできた感じでしょうか。
オマケ:モックを使ったBeanのテスト
ここまでSpring Web MVCのテストを見てきましたが、ふつうのBeanのテストのパターンを完全に飛ばしています。
軽くやっておきましょう。単体テストな感じであればインスタンスをnew
しても良いと思うのですが、今回はモックを使ってみます。
Core Features / Testing / Testing Spring Boot Applications / Mocking and Spying Beans
まずは、Serviceをもうひとつ追加しましょう。
src/main/java/org/littlewings/spring/webmvc/WordsJoinService.java
package org.littlewings.spring.webmvc;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@Service
public class WordsJoinService {
MessageDecorationService messageDecorationService;
public WordsJoinService(MessageDecorationService messageDecorationService) {
this.messageDecorationService = messageDecorationService;
public String join(String... words) {
return messageDecorationService.decorate(Arrays.stream(words).collect(Collectors.joining(" ")));
このServiceでは、最初に作成したServiceを使っています。
テストはこちら。
src/test/java/org/littlewings/spring/webmvc/MockingBeansTest.java
package org.littlewings.spring.webmvc;
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 static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.BDDMockito.given;
@SpringBootTest
public class MockingBeansTest {
@Autowired
WordsJoinService wordsJoinService;
@MockBean
MessageDecorationService messageDecorationService;
@Test
public void serviceTest() {
given(messageDecorationService.decorate("Hello World"))
.willReturn("*** Hello World !!***");
assertThat(wordsJoinService.join("Hello", "World"), is("*** Hello World !!***"));
テストクラスは@SpringBootTest
アノテーションを使って作成して、テスト対象のBeanを@Autowired
しつつ、依存するBeanは
@MockBean
アノテーションでモック化すればOKです。
Spring Web MVCで作成したアプリケーションのテスト方法を見てみました。
あまり理解できていなかったので、時間を取って今回整理してみて良かったかなぁと思います。