きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Wiremockをモックサーバとして使用してJavaの単体テストをする

弊社ではモックサーバにWiremockを使用しています。

この記事ではWiremockを知ってもらうきっかけを提供することを目的としています。詳しいことは他の方の記事を参考にした方が良いでしょう。

ゴール

  • WiremockのモックサーバでJUnitでテストする

Wiremockのスタンドアロンで起動する機能については、今回は紹介しません。スタンドアロンなら別のいいツールあるかもしれないので。

Wiremockとは

WireMockはHTTPモックサーバー。リクエスト回数やリクエストJSONを確認することもできる。

スタンドアロンとしても立ち上げることができるので、JUnitとの連携だけでなくE2Eテストや検証環境でも使用することができる。

環境

  • Java
    • 15
  • org.springframework.boot
    • 2.4.0
  • com.github.tomakehurst:wiremock-jre8
    • 2.27.2
  • ru.lanwen.wiremock:wiremock-junit5
    • 1.3.1

単体テストでの使い方

Wiremockが使えるように依存関係に含める

使用できるように依存関係に含めます。デフォルトだとJUnit4の記述しかできないので、JUnit5でも使用できるように関連ライブラリも依存関係に含めます。

Gradleでの記述方法。

testImplementation "com.github.tomakehurst:wiremock-jre8:2.27.2"
testImplementation "ru.lanwen.wiremock:wiremock-junit5:1.3.1"

Wiremockをテストクラスで使えるようにする

WiremockResolver.classを読み込みます。

@ExtendWith(WiremockResolver.class)

Wiremockサーバを初期セットアップする


テストクラスのコンストラクタを使用します。

@WiremockResolver.Wiremockをパラメータに付けることで、初期化対象となります。

ランダムポートで生成されるので、テストターゲットに対してパラメータを渡してあげることで並列実行等もできるようになります。

  private WireMockServer server;

  ZipCloudClientImplTests(@WiremockResolver.Wiremock WireMockServer server) {
    this.server = server;
  }
  @BeforeEach
  void setup() {
    ZipCloudClientProperties properties = new ZipCloudClientProperties();
    properties.setSchema("http");
    properties.setHost("localhost");
    properties.setPort(server.port());
    properties.setPath("/api/search");

    RestOperationFactory restOperationFactory = new RestOperationFactory(restTemplateBuilder, new RestTemplateInterceptor());
    RestOperations restOperations = restOperationFactory.createRestOperations(properties);

    target = new ZipCloudClientImpl(properties, restOperations);
  }

モックをセットする


あとはWiremockに設定するのみです。

こちらを設定したうえで、テストを実施すると対向システムをモックサーバとした状態でテストできます。

次の記載方法は、特定のURLとクエリパラメータ時に、特定のResponseBodyを返却する書き方となります。

    String responseBody =
        """
            {
              "message":null,
              "results": [{
                "address1":"神奈川県",
                "address2" : "厚木市",
                "address3" : "中町",
                "kana1" : "カナガワケン",
                "kana2" : "アツギシ",
                "kana3" : "ナカチョウ",
                "prefcode" : "14",
                "zipcode" : "2430018"
              }],
              "status": 200
            }""";

    server.stubFor(
        get("/api/search?zipcode=2430018").willReturn(
            aResponse()
                .withStatus(200)
                .withHeader("Content-Type", "text/plain")
                .withBody(responseBody)
        )
    );

リクエストJSONを比較したい時には、withRequestBodyを使用します。※ソースコードでは比較していません。

    String requestBody =
        """
            {
              "message":null,
              "results": [{
                "address1":"神奈川県",
                "address2" : "厚木市",
                "address3" : "中町",
                "kana1" : "カナガワケン",
                "kana2" : "アツギシ",
                "kana3" : "ナカチョウ",
                "prefcode" : "14",
                "zipcode" : "2430018"
              }],
              "status": 200
            }""";

    server.stubFor(
        post("/api/search")
            .withRequestBody(
                equalToJson(requestBody)
            )

ソースコード

終わりに

Wiremockが使えると単体テストができるようになるので便利です。

もちろん、できるだけ本番環境でテストできることが最高ですが、相手のサービスのメンテナンス時間が原因でテスト失敗するFlakyなテストになってしまいます。実行するたびに課金されるようなAPIを実行しても厳しいですしね。

知らないだけでもっと便利なJavaのモックサーバはあるかもしれませんが、Spring公式でも紹介されている以上、便利なものでしょう。

JavaとWiremockで調べる人が多いとは思いませんが、一助になればと思います。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考

Wiremock公式サイト wiremock.org

Spring公式のWiremockの使用方法 cloud.spring.io

f:id:nainaistar:20211009232958p:plain

【Pytest】不安定なテストに失敗したら自動リトライさせる(flaky)

以前、Javaで不安定なテスト(flakyなテスト)をリトライさせるライブラリを素振りしました。今回の記事はPython版です。

Pytestの公式ページにてFlakyなテストに対する解決策となるライブラリを複数提示されており、その中でもGitHubのStar数が一番多かったflakyを素振りすることにします。

ユースケース

E2E試験等の不安定(flaky)なテストを、失敗時に再実行させて全体として安定させたい。

環境

  • Python
    • 3.8.5
  • pytest
    • 6.2.4
  • flaky
    • 3.7.0

インストール方法

pip install flaky

設定値

max_runs

最大何回まで再実行するかを設定する。デフォルト値は2回。

min_passes

何回テストが成功するまで再実行するか設定する。規定数まで成功したら、それ以降のテストは行わない。デフォルト値は1回。

rerun_filter

再実行する際に実行したいメソッドを設定する。自分で振る舞いを設定できるので自由に設定できます。flaky公式ページでは、2例紹介されていました。

  • 実行するたびに安定性のために1秒waitする
  • 特定のエラーが発生したら終了する

実行するたびに安定性のために1秒waitする

import time

def delay_rerun(*args):
    time.sleep(1)
    return True

@flaky(rerun_filter=delay_rerun)
def test_something_else():
    assert 1 == random.choice([1, 2, 3])

特定のエラーが発生したら終了する

RuntimeErrorが発生したら1回で終了する。flaky公式では自作例外を使用していました。

def is_not_crash(err, *args):
    return not issubclass(err[0], RuntimeError)

@flaky(rerun_filter=is_not_crash)
def test_something_else():
    raise RuntimeError

使い方

テストごとにアノテーションで設定

メソッドに@flakyアノテーションを付与します。設定については、上に記載しているとおりです。

from flaky import flaky

def rerun_log(err, *args):
    print("*再実行します*")
    return True

@flaky(max_runs=3, min_passes=2, rerun_filter=rerun_log)
def test_1():
    assert 1 == 2

CLIで設定

CLIから再実行設定する

pytestのオプションに--force-flakyを付与することで、各テストにアノテーションを付与していなくても、再実行設定を付与できます。デフォルト値で再設定します。

pytest --force-flaky 

CLIから詳細な再実行設定をする

pytestのオプションで--force-flakyを設定し、--max-runs=nun--min-passes=numを付与すると最大実行数、規定数を設定できます。

ただし、アノテーションで付与している設定値の上書きはできません。また、rerun_filterに関してはCLI上からは設定できません。

pytest --force-flaky --max-runs=4 --min-passes=2

アノテーションで付与した設定を無視する

アノテーションで付与した設定を無視できます。

テストを作りこんでいる最中に再実行設定が有効になっていると、TDD等でテストを快適に書けませんので、こちらの設定には大きな価値があります。

pytest -p no:flaky

その他

再実行したテストのテストレポートの出し方を変更できます。

# 再実行したテストはテストレポートに出さない
--no-flaky-report
# 再実行したテストは短いテストレポートにする
--no-success-flaky-report

ソースコード

終わりに

現在、SalesforceをE2Eでテストしているのですが、Salesforce側のエラーでFlakyになるので困っています。手動ならリトライは簡単なんですがね…。

今回のライブラリ:flakyを入れたことで安定して稼働してほしいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考記事

類似記事

f:id:nainaistar:20210824231058p:plain

Ansibleで失敗時のエラーメッセージをカスタマイズしたい

※ もっといいやり方があれば教えてください。


この記事では、Ansibleでエラーメッセージをカスタマイズするやり方を記します。

正確な表現としては異なるので、あくまで「したい」です。

環境

  • Ansible
    • 2.11.5

ゴール

  • 処理失敗時に、目的のエラーメッセージを出力する

対応

実行するタスクとエラーメッセージを出力するタスクの2つを用意する

失敗時のエラーメッセージを直接加工できるオプションはなさそうでした。

ですので、コマンドを実行するタスクと、実行結果をもとにエラーメッセージを出力するタスクを分ける必要があります。

実行するタスクの設定

実行するコマンドをcommandに設定してください。

次に、後続のタスクでメッセージをハンドリングしたいため、registerで結果を変数に設定してください。

次に、failed_when: falseもしくは、ignore_errors: trueのどちらかを設定してください。オプションの意味は次のとおりです。

  • failed_when
    • 失敗の条件をカスタマイズするためのオプション。たとえば、「diffでファイルの差分がない状態をエラーとしたい」場合は、通常のコマンドだとエラーにならないため、failed_whenオプションで異常とする。等々のユースケースがあります。今回は、後続処理に回したいため、あえてエラーとしないfalseを設定します。
  • ignore_errors
    • エラー無視設定のオプション。trueに設定しておくことで、エラーが発生しても後続処理を実行してくれます。ただし、エラーメッセージ自体は出力されます。
    - name: "コマンドを実行するタスク"
      command: "実行コマンド"
      register: result
      failed_when: false # エラーメッセージを出力しない
      ignore_errors: true # エラーが発生しても無視する

エラーメッセージを出力するタスクの設定

whenを使うことで、タスクの実行条件を設定できます。先ほどのコマンドの実行結果をresultに設定していますので、result.rc != 0と設定すると、コマンドに失敗した時だけタスクを実行します。

次に、failを設定し、実行しているタスクではなく、エラーメッセージのタスクで失敗させます。こちらのオプションのmsgに値を設定することで、エラーメッセージをカスタマイズできます。

詳細なエラーが欲しい場合は、実行結果に設定しているresultを出力すればよいでしょう。

    - name: "エラーメッセージをカスタマイズする"
      when: result.rc != 0
      fail: 
        msg: "{{ result }} 
        エラーメッセージをカスタマイズする"

hogehogeという存在しないコマンドを実行しています。

TASK [コマンドを実行するタスク] *************************************************************************************************************************************************************************
ok: [localhost]

TASK [エラーメッセージをカスタマイズする] ***************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "{'rc': 2, 'stdout': '', 'stderr': '', 'cmd': 'hogehoge', 'failed': False, 'msg': \"[Errno 2] No such file or directory: b'hogehoge'\", 'stdout_lines': [], 'stderr_lines': [], 'changed': False, 'failed_when_result': False} エラーメッセージをカスタマイズする"}

PLAY RECAP **********************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

ソースコード

使い方はREADME.MDに載せています。GitHub Codespacesで実行しました。

pip install ansible
ansible-playbook -i localhost error_message.yml 

終わりに

正直、Ansibleでエラーメッセージを加工したい、という要件はかなり限定的だと思います。

そもそもなぜこのユースケースが必要だったかというと。今の職場では、AnsibleでGit pullして最新化してからビルドしてデプロイしようとしています。この「Git pull」でIDと発行したトークンを使用しているのですが、現状としては開発者個人に紐づいてしまっています。本来であれば、デプロイ用のIDとトークンを用意したほうがよいのは理解していますが、管理が煩雑になるため用意しませんでした。

ただ、現状では動いているものの、開発者個人が退職したタイミングでAnsibleのタスクが動かなくなってしまいます。出力されるエラーメッセージも「認証エラー」しか出力されないため、退職してトークンが無効化されたという真因にたどり着くのは時間がかかります。

そのリスクを低減するために、エラーメッセージに障害対応用のQ&Aサイトへのリンクを貼りたかった。という要望が開始地点でした。

まぁ、これで対応できたのはいいものの、日本語メッセージが文字化けして効率的に誘導できなかったんですがね…。

他のユースケースで使えないかもしれませんが、かなりハマったのでメモとしてブログに残すことにします。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考情報

f:id:nainaistar:20210928225009p:plain