CircleCIのparallelismを利用してサーバーレスにおけるインテグレーションテストの遅さを解決した話

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

サーバーレスにおいてインテグレーションテストはますます重要になってきています。この命題はいくつかの場所ですでに議論されてきたため、お聞きしたことがあるかもしれません。簡単に言うと、サーバーレスアプリケーションはクラウドベンダ(AWSのような)が提供する多くのフルマネージドサービスから構成されるため、ローカル環境でそのサービスをシミュレートするのは難しいあるいは不可能だからです。確かに、LocalStackSAM CLI、類似のツールがこの問題に対して取り組んできましたが、少なくとも今のところ、これらのツールは実環境上のサービスを置き換えるほどのパワーは持っていまません。なぜならいくつかのサービスや機能が不足しているからです。

したがって、ほとんどの場合インテグレーションテストは実環境に対して実行されます。なぜならその他に選択肢がないから。これは今のところ合理的な戦略です。しかし、別の問題もまた生み出します。この問題のうちの1つはテストフィードバックの遅さです。テスト対象に対するネットワークアクセスのためインテグレーションテストはユニットテストに比べてとても遅いです。私達はこの問題に対して手こずっていました。ソフトウェアの品質を向上させるためにはテストを書くべきですが、その実行時間を遅くさせてしまいます。

この問題をよりわかりやすくするために、私達のプロジェクトでどのようにインテグレーションテストを実施しているのか説明します。以下の図を見てください。

これが私達の開発しているアーキテクチャです。主にAmazon API Gateway、AWS LambdaそしてAmazon DynamoDBから構成されるAPIを作成しています。実際のところ、他のAWSサービスも使っているのですがこのポストには関係ないためその点は触れません。

とにかく、クライアントはモバイルアプリです。このクライアントはAPIを通してバックエンドシステムと通信することができます。問題点はAPIのインテグレーションテストです。このテストが行うことはHTTPクライアントがAPIへアクセスしその結果を評価することです。完了するまで13分も時間がかかっていました。かなり遅いですね。

このポストではCircleCIの機能を利用しどのようにこの問題を解決したのかご紹介したいと思います。

Parallelism in CircleCI

テストの実行時間を低減させるために、私達はCircleCIのparallelismの機能を使うことに決めました。これは私が知る限り、テストを平行に実行するための最も簡潔で便利な方法です。すべてのテストでpytestを使っているためプラグイン(pytest-xdistpytest-parallelなど)を活用することもできます。しかし、CircleCIのparallelismのアドバンテージの1つはプログラミング言語に依存していないという点です。どの言語であっても一緒に使いことができます。別の言語を選択することを考慮し、私達はこの機能を使うことに決めました。

まぁ、話を戻しましょう。この機能を利用するためには2つのステップに従うことが必要です。このステップをより詳細に見てみましょう。

最初に、ジョブの中で parallelism のセクションを設定する必要があります。これはどのくらい多くのコンテナを並列に実行するかという意味です。このセクションはジョブのレベルでなければならない点に注意してください。CircleCIは現在のところステップレベルではparallelismをサポートしていません。したがって、ジョブの粒度を考慮する必要があります。例えば、もし平行に実行する必要のない多くのステップが存在した場合、parallelismの利点を低減させてしまうでしょう。この利点を得たい場合はこういったステップをジョブから分離する必要があります。

使い方はとても簡単です。必要なことはジョブの中で parallelism のセクションを指定するだけです。例えば以下のような感じです。

version: 2
jobs:
  test:
    docker:
      - image: circleci/<language>:<version TAG>
    parallelism: 4

次はCircleCI CLIです。このツールは主にローカル環境でのデバッグや設定ファイル( config.yml )のバリデーションに使われます。また tests コマンドがあり、そのコマンドは2つのサブコマンドを持っています。 globsplit です。 glob サブコマンドはテストファイルのディスカバリに使われ、 split サブコマンドはテストファイルをコンテナ間で分散するために利用されます。

より詳細に説明してみます。

glob サブコマンドはテストフレームワークが通常提供しているテストディスカバリに似ています。テストファイルをフィルタするためにワイルドカードを扱えます。例えばこういった感じで使えます。

$ circleci tests glob "tests/integration/test_*.py"
tests/integration/test_foo.py
tests/integration/test_bar.py
tests/integration/test_baz.py
tests/integration/test_blah.py
...

split サブコマンドは引数として渡されたテストファイルがどのようにコンテナ間で分散されるのか決定します。通常のパターンは glob サブコマンドの結果を split サブコマンドの標準出力に渡すことです。以下の例を見てください。

$ circleci tests glob "tests/integration/test_*.py" | circleci tests split

この章では、CircleCIにおけるparallelismの使い方をご紹介しました。続いて、私達のプロジェクトでテストの実行時間を短縮するためこの機能をどう活用したのか説明します。

Parallelism in Integration Tests

上述したように私達はテストに pytest を利用しています。 pytest のテストファイルに test というプレフィックスを付け、 tests/integration ディレクトリの中に置いてます。したがって、以下のような感じでCircleCIの設定ファイルを作ることができるでしょう。

  build_and_test_unit:
    parallelism: 2
    <<: *build_and_test_container
    steps:
...
      - run:
          name: Run integration tests
          command: |
            source aws-envs.sh
            source .venv/bin/activate
            python -m pytest $(circleci tests glob "tests/integration/test_*.py" | circleci tests split)

テストファイルを引数として pytest に渡し、コンテナ間でそれらを分散させているだけです。この設定ファイルがジョブをどのように扱っているのか説明します。以下の図を見てください。

各ステップの最後に数字があることに気づいたかもしれません。これがコンテナのインデックスです。この場合、2つのparallelismを指定したので2つのコンテンが起動しています。また実行時間も見てください。以前はより時間がかかっていたにもかかわらず5分まで短縮することができました。

そして最後に以下の図を見てください。同じステップが別のタスクを実施していることに気付くでしょう。つまり、 split サブコマンドがタスクをコンテナ間で分散しています。

まとめ

このポストではCircleCIのparallelismにおける機能と実際の事例について議論しました。私は多くの状況でこの機能が適用できると思っています。このポストがお役に立てれば幸いに思います。