背景
マイクロサービスにより構築されたwebアプリケーションのテストコードではサービス間の通信が発生するケースがある。 これまではmockingjay-serverを利用して外部サービスのmockを作成しサービステストを実装していたが、エラー発生ケースのmock定義が非常に煩雑になると感じた。 且つ、マイクロサービス間で頻繁に呼ばれるAPIに関してはエラーケースを定義することで他のテストケースへの影響も発生した。 これらのことから、テストケース間で依存関係を与えない方法でサービステストが実装出来ないかを調査したところWireMockが候補に挙がったためシンプルではあるが実装例を残しておく。
実行環境
導入手順
本検証では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が導入コスト面から言ってもおすすめである。
課題
※外部のmockサーバーに管理している場合は、mockを用いたテスト時にテストが失敗するため検知できる。 ※今回のように利用する場合は、サービス間の関連図を作成しておく必要があると思う。