【Webアプリ】YOUTRUSTのテストのリアル🔍

こんにちは、YOUTRUST Webエンジニアの寺井(YOUTRUST/X)です。

みなさんの組織ではテストコードについて以下のような問題が生じていないでしょうか?

  • 大事な部分のテストが書かれていない
  • テストの実行時間がとても長くなってしまっている
  • 成功したり失敗したりするようなFlakyなTestが存在する

YOUTRUSTではWebバックエンドのテストコードに関していくつか工夫していることがあるため、今回はYOUTRUSTのテスト事情を公開したいと思います。

1. 結合テストとE2Eテストは書かず、単体テストを網羅的に書いている

YOUTRUSTのバックエンドでは、結合テストとE2Eテストは書かずに単体テストを網羅的に書いています。

具体的にどのようなテストを書いているかを説明するために、まずはYOUTRUSTのプロダクションコードの説明から始めたいと思います。

まずYOUTRUSTのWebバックエンドはRailsで実装しており、テストはRSpecで書いています。

そして、YOUTRUSTではCQRS(Command Query Responsibility Segregation)という考え方に従ってプロダクトの実装を行っています。

具体的には、参照系の場合はControllerのアクションからQueryを呼び出し、更新系の場合はControllerのアクションから対応するUseCaseを呼び出し、さらにそのUseCaseから必要に応じて複数のCommandを呼び出す最大3階層の設計になっています。

Controller・UseCase・Commandの3階層

この設計については、創業エンジニアのやまでぃさんこちらの記事で詳細に説明されていたり、GitHubサンプルコードが公開されているので、ご興味がある方はそちらをご参照ください。

1-1. Controller層のテスト

Controller層のテストはspec/requests/にRequest Specとして書いており、主に以下の2点を確認しています。

HTTPレスポンスステータスコードでは、正常系のときに200が返るか、未認証のときに401が返るかなどをテストしています。

expect(response).to have_http_status(:ok)
expect(response).to have_http_status(:unauthorized)

また、JSONを返すエンドポイントの場合はそのJSONオブジェクトの形式のテストも行っています。

expect(json['hoge_users']).to match(
  [
    {
      'id' => hoge_user1.encrypted_id,
      'name' => hoge_user1.name,
    },
    {
      'id' => hoge_user2.encrypted_id,
      'name' => hoge_user2.name,
    },
  ],
)

1-2. UseCase層のテスト

UseCase層のテストはspec/use_cases/に書いており、

【正常系の場合】

  • エラーが発生しないこと
  • Commandを呼び出すこと

【異常系の場合】

  • エラーが発生すること
  • UseCaseのバリデーションに引っかかる場合はCommandを呼び出すこと

などを主にテストしています。

1-3. Command層のテスト

Command層のテストはspec/commandsに書いており、

【正常系の場合】

  • エラーが発生しないこと
  • データの作成や削除が行われること

【異常系の場合】

  • エラーが発生すること
  • データの作成や削除が行われないこと

などを主にテストしています。

1-4. フロントエンドのテスト

YOUTRUSTでは現在、Webフロントエンドではごく一部の共通処理のみテストコードを書いています。

describe('isInWeek', () => {
  test('trueが返ってくること', () => {
    const now = new Date();
    now.setDate(now.getDate() - 6);

    expect(isInWeek(now)).toEqual(true);
  });

  test('falseが返ってくること', () => {
    const now = new Date();
    now.setDate(now.getDate() - 8);

    expect(isInWeek(now)).toEqual(false);
  });
});

ごく一部の共通処理を除いてテストコードを書いていない理由としては、Webフロントエンドの変更は頻度が多く挙動の担保よりも実装のスピードを優先していることや、そもそも複雑なロジックは可能な限りバックエンドに寄せていることがあります。

もちろんWebフロントエンドのテストを書くメリットも大きいので、随時検討していく必要性はあると思います。

2. 単体テストの実装を徹底しているため、テストのカバレッジが高い

1で書いたように、YOUTRUSTではWebバックエンドにおいては基本的にすべてのプロダクションコードに対してテストコードを書いています。

  • Modelに修正を加えたときはModelのテスト
  • Controllerに修正を加えたときはControllerのテスト
  • UseCaseに修正を加えたときはUseCaseのテスト
  • Commandに修正を加えたときはCommandのテスト
  • Queryに修正を加えたときはQueryのテスト

がセットで書かれていることを必ずコードレビューでチェックしています。

そのため、テストのカバレッジは高い状態を保つことができており、rails statsコマンドで確認できるCode to Test Ratio(プロダクションコードの行数に対するテストコードの比率)は3.1という数値になっています。

rails statsの結果(2023/8/24時点)

Code to Test Ratioの数値は、2022年のSmartHR社では3.8*1、2017年のクックパッド社では1.4*2となっているので、おそらく他社と比べても悪くない数値になっているのではないかと思います。

3. モックを駆使しているため実行速度が速い

2で書いたようにYOUTRUSTでは多くのテストを書いていますが、テスト全体の実行時間はかなりの短さを保てています。

その秘訣は積極的に使っているモックにあります。

ControllerではUseCaseをモックしてUseCaseの呼び出しまでが行われることを、UseCaseではCommandをモックしてCommandの呼び出しまでが行われることをテストしています。

result = instance_double(HogeUseCase, success?: true)
allow_to_receive_mocked_perform(HogeUseCase).and_return(result)

(※allow_to_receive_mocked_performはMockHelpersで独自に定義しているメソッドです。)

Comandによるデータの変更は、上位のUseCaseやControllerのテストでは見ていません。

しかしそれでも単体テストを十分に網羅できているため、テストの不足が問題となったことは私が入社してからは一度も発生していません。

2023年8月現在、ローカル環境ですべてのテストを5並列で実行したとき、全6213個のテストを終えるまでにかかる時間はおよそ2分30秒となっています。

最後に

いかがでしたでしょうか?

テストには様々な考え方があり、どんなプロダクトにおいても共通のたった一つの正解というものはないのかなと思っています。

YOUTRUSTではこれまで様々なメリットとデメリットを天秤にかけて今の方法にたどり着いています。

もちろん、より良い方法がないかを常に検討して改善していく必要はありますが、私が入社してからの5ヶ月間まだFlakyなテストには遭遇していないことや、テストが不足していたことが原因で発生してしまった障害なども存在していないので、とてもバランスの良いテスト設計になっていると思います。

また、私自身テストに関する体系的な学習はこれまで後回しにしてきてしまっているので、テストに対してもどこかでじっくり深掘った学習を行いたいと思いました。

今回はテストについて書きましたが、DevOps専門の人がいなくても全員が開発環境の改善を意識しており、気持ちよく開発ができる環境が整ったYOUTRUSTにもしご興味があれば、カジュアル面談などでぜひお待ちしております!

herp.careers