SRE兼スクラムマスターのブログ

チーム開発が好きなエンジニアブログです。検証した技術やチームの取り組みを書いていきます。

WireMock を使って通信が発生する サービステスト を実装する

背景

マイクロサービスにより構築された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>

サンプル実装

今回はマイクロサービスで構築されたアプリケーションを想定してサービステストコードを実装する。

前提条件

  • コードのロジックにおいて外部サービスの呼び出しが発生すること
  • 外部サービスの呼出し結果により処理が変更される

    処理イメージ

    image.png

  • 上記処理の場合、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());
    }
  • 各テストコードが成功していることが分かる image.png

感想

当初の想定通りテストケース間で依存を与えずにサービステストの実装を行えた。かつ導入手順もシンプルである為非常に扱いやすい。 mock管理のフレームワークといえばswaggerが有名であるが、今回のようなテスト目的のみであればWireMockが導入コスト面から言ってもおすすめである。

課題

  • テストに利用するAPIの定義が変更された場合(URIが変更されるなど)、該当するAPIがどのサービスから呼び出されているか管理できていないと発見が遅れること。

※外部のmockサーバーに管理している場合は、mockを用いたテスト時にテストが失敗するため検知できる。 ※今回のように利用する場合は、サービス間の関連図を作成しておく必要があると思う。

その他