背景
マイクロサービスにより構築されたwebアプリケーションのテストコードではサービス間の通信が発生するケースがある。
これまではmockingjay-serverを利用して外部サービスのmockを作成しサービステストを実装していたが、エラー発生ケースのmock定義が非常に煩雑になると感じた。
且つ、マイクロサービス間で頻繁に呼ばれるAPIに関してはエラーケースを定義することで他のテストケースへの影響も発生した。
これらのことから、テストケース間で依存関係を与えない方法でサービステストが実装出来ないかを調査したところWireMockが候補に挙がったためシンプルではあるが実装例を残しておく。
実行環境
- WireMock 2.17.0
- Java 1.8.0_162
- JUnit 4.12
導入手順
本検証ではmavenを利用しているため、公式サイトの手順に沿って下記の依存性をpomに記入する。
後はmvn install
で依存ライブラリをインストール出来る。
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>2.17.0</version>
<scope>test</scope>
</dependency>
注意点
- 他の依存ライブラリでjettyを利用しているものが存在する場合、jettyのバージョンの関係でwiremockが起動しないケースが発生した。
java.lang.NoClassDefFoundError: org/eclipse/jetty/util/thread/Locker
at org.eclipse.jetty.server.Server.<init>(Server.java:94)
at com.github.tomakehurst.wiremock.jetty9.JettyHttpServer.createServer(JettyHttpServer.java:118)
at com.github.tomakehurst.wiremock.jetty9.JettyHttpServer.<init>(JettyHttpServer.java:66)
at com.github.tomakehurst.wiremock.jetty9.JettyHttpServerFactory.buildHttpServer(JettyHttpServerFactory.java:31)
at com.github.tomakehurst.wiremock.WireMockServer.<init>(WireMockServer.java:74)
at com.github.tomakehurst.wiremock.junit.WireMockClassRule.<init>(WireMockClassRule.java:32)
at com.github.tomakehurst.wiremock.junit.WireMockClassRule.<init>(WireMockClassRule.java:40)
at spark.unit.service.ServiceTests.<clinit>(ServiceTests.java:26)
at sun.misc.Unsafe.ensureClassInitialized(Native Method)
at sun.reflect.UnsafeFieldAccessorFactory.newFieldAccessor(UnsafeFieldAccessorFactory.java:43)
at sun.reflect.ReflectionFactory.newFieldAccessor(ReflectionFactory.java:156)
at java.lang.reflect.Field.acquireFieldAccessor(Field.java:1088)
at java.lang.reflect.Field.getFieldAccessor(Field.java:1069)
at java.lang.reflect.Field.get(Field.java:393)
at org.junit.runners.model.FrameworkField.get(FrameworkField.java:73)
at org.junit.runners.model.TestClass.getAnnotatedFieldValues(TestClass.java:230)
at org.junit.runners.ParentRunner.classRules(ParentRunner.java:255)
at org.junit.runners.ParentRunner.withClassRules(ParentRunner.java:244)
at org.junit.runners.ParentRunner.classBlock(ParentRunner.java:194)
at org.junit.runners.ParentRunner.run(ParentRunner.java:362)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.util.thread.Locker
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 30 more
- 対応策①
- 下記の依存ライブラリをpomに追加する
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.3.14.v20161028</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>9.3.14.v20161028</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.3.14.v20161028</version>
<scope>test</scope>
</dependency>
- 対応策②
-
wiremock-standalone
の依存ライブラリをpomに追加する
-
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
サンプル実装
今回はマイクロサービスで構築されたアプリケーションを想定してサービステストコードを実装する。
前提条件
- コードのロジックにおいて外部サービスの呼び出しが発生すること
- 外部サービスの呼出し結果により処理が変更される
処理イメージ
- 上記処理の場合、Aサービスの「/user/insert」に対するサービステストを実装する場合、テスト結果がBサービスに依存することがわかる。
- 従来のテストではmockサーバーにBサービスの正常系・異常系を定義することでCDCテストが可能になるが、異常時にURIやリクエストパラメータの定義が固定値になりがちである。
※個人的な経験上です… - wiremockを利用することによってテストメソッド単位でBサービスのmockを定義できるため、mockサーバーに定義することなくかつ他のテストに依存することなくテストコードが実装できる。
- 実装例ではメソッド毎にBサービスの結果を変更することでテストメソッド内のみでmockを利用している。
実装例
// wiremockをポートを指定して定義する
@Rule
public WireMockClassRule wireMockRule = new WireMockClassRule(8082);
@Test
public void TEST_Bサービス正常動作時() throws UnirestException {
// Bサービスの正常動作をmockとする
wireMockRule.stubFor(get(urlEqualTo("/mock/sample"))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("OK")));
HttpResponse<String> res = Unirest.post("http://localhost:8081/user/insert")
.header("Content-Type", "application/json")
.asString();
assertEquals("response codeが200であること:", 200, res.getStatus());
assertEquals("response bodyがOKであること:", "OK", res.getBody());
}
@Test
public void TEST_Bサービス異常動作時() throws UnirestException {
// Bサービスの異常動作をmockとする
wireMockRule.stubFor(get(urlEqualTo("/mock/sample"))
.willReturn(
aResponse()
.withStatus(400)
.withHeader("Content-Type", "application/json")
.withBody("B Service Error")));
HttpResponse<String> res = Unirest.post("http://localhost:8081/user/insert")
.header("Content-Type", "application/json")
.asString();
assertEquals("response codeが400であること:", 400, res.getStatus());
assertEquals("response bodyがB Service Errorであること:", "B Service Error", res.getBody());
}
感想
当初の想定通りテストケース間で依存を与えずにサービステストの実装を行えた。かつ導入手順もシンプルである為非常に扱いやすい。
mock管理のフレームワークといえばswaggerが有名であるが、今回のようなテスト目的のみであればWireMockが導入コスト面から言ってもおすすめである。
課題
- テストに利用するAPIの定義が変更された場合(URIが変更されるなど)、該当するAPIがどのサービスから呼び出されているか管理できていないと発見が遅れること。
※外部のmockサーバーに管理している場合は、mockを用いたテスト時にテストが失敗するため検知できる。
※今回のように利用する場合は、サービス間の関連図を作成しておく必要があると思う。