SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

RSpecの実行時間を短縮した話

はじめに

こんにちは!スマートキャンプ開発エンジニアの末吉(だいきち)です。

私がチームメンバーとして日々開発している「BALES CLOUD」では、RSpecを割としっかりと書いております。
ただ近頃、RSpecの実行時間が長くなってきており、開発に少し支障をきたすようになってきました。
そこで開発の生産性を上げるべく、RSpecの実行時間短縮を試みたので、
今回は、こちらの件についてお話ししたいと思います!

まずは結果

  • Before: 40〜50[min]
  • After: 11〜18[min]

上記通り、30[min]ほど短縮できました!
参考までに、circleciでのテスト実行時間推移を貼っておきます。(対応時期:9月14日〜10月3日)

circleciでのテスト実行時間推移

またこれは狙ったわけではないのですが、テスト実行時間を短縮できたことにより、circleciのcredit使用量も減らせました!
参考までに、circleciのcredit使用量推移を貼っておきます。(対応時期:9月14日〜10月3日)

circleciのcredit使用量推移

遅かった要因と改善方法

今回行った大きな対応が3つあるため、その対応と背景について書いていきたいと思います。

対応1:生成するテストデータを減らした。

まずは1つ目。

背景

ある特定クラスのテストにおいて、一通りの標準的なデータを、すべてのケースで作成する構成となっておりました。
具体的には、最上位階層部分でlet!が使用されていたり、FactoryBotのコールバックでデータを生成していたりという感じです。
このような構成にすることで、DRYに書けることがあったりなどのメリットはあるかと思います。
しかし今回は、データ準備に1ケースあたり30[sec]近くかかっていたため、過剰な状態となっておりました。

対応

以下を行い、各テストケースで作成するデータを削減しました。

  • 最上位階層のlet!letに変更し、データが必要なケースでのみデータを生成するようにした。
  • FactoryBotコールバックで生成するデータを減らした。
    (今回は行いませんでしたが、Active Recordのコールバックでデータ生成している場合は、テストにおいてはそちらを停止することも検討してみて良いか思っております。)

結果

これで8[min]ほど削減できました!

対応2:観点外の処理をMock化し、テスト観点において不要な処理が実行されないようにした。

次に2つ目。

背景

バリデーション観点のテストで2000件のデータを作成しており、
これによってテスト対象の処理が2000件分行われていました。

対応

バリデーション観点のテストでは、メイン処理部分をMockにしました。

これによって、大量データで確認する部分を最小限にできました。
また大量データを生成する際は、create_listをやめて、build_list + importを使うようにしました。
create_listでは指定した件数分のクエリが発行されます。
build_list + importにすることで、発行されるクエリの件数を減らすことができ、データ準備の時間を削減できました。

結果

これで7[min]ほど削減できました!

対応3:テストの実行を並列化した。

一番効果のあった対応がこちらであり、導入も簡単であったためおすすめです!

背景

対応1」「対応2」によって、特定のテストに極端な時間がかかることは無くなりました。
ただし実行時間がゼロになることないので、テストの総量が多い場合は、
「書き方を工夫しての実行時間の削減」には限界があります。
BALES CLOUDでは冒頭では書いたとおり、割としっかりRSpecを書いております。
そのため、「書き方の工夫」だけでこれ以上大幅な改善をするのは、難しい状況にありました。

対応

paralell_testsを使用してテストの実行を並列化しました。

なぜparalell_testsにしたか?

並列化はcircleciで行なう方法もあったのですが、今回はparalell_testsによる並列化を選択しました。
理由としては、以下です。

  • 導入が簡単そうであったこと。
  • circleciで並列化する場合は、使用クレジットの増加が考えられるが、paralell_testsで並列化する場合は、使用クレジットの削減の可能性もあること。
  • circleciから別のサービスへ乗り換えた場合に、paralell_testsで並列化しておくと、無駄にならないこと。
導入 ~ 手順 ~

手順については、すでにさまざまな方が紹介しているためここでは省略しようかと思います。

導入 ~ 困ったこと ~

並列化の導入準備が終わり、いざ実行してみると、テストが落ちてしまいました。
「直列で全テストを流した場合」および、「単体でテストを実行した場合」では成功するテストが、
「並列で全テストを流した場合」には、失敗してしまう状況です。

  • 原因
    BALES CLOUDではテストにwebmockを導入しているのですが、
    afterWebMock.disable!している箇所があり、後続のテストでスタブが使われておらず、落ちておりました。
  • 対応
    スタブが必要な箇所にWebMock.enable!を追加して対応しました。
    afterでのWebMock.disable!を取り除く方法もあり、最終的には方針を決めてコードも揃えたいと思っておりますが、
    今回の目的とは異なるため、いったんWebMock.enable!を追加する方法で対応しました。
導入 ~ 並列数の決定 ~

並列数は結果としては「4」にしました。
決定方法としましては、「2 -> 4 -> 6」と試していき、「4」が頭打ちとなりそうであったため決定しました。

結果

これで20[min]ほど削減できました!

実は「対応1」「対応2」の前にいったん試したときは、7[min]ほどしか改善しませんでした。
しかし、「対応1」「対応2」によって極端に重いテストを取り除くことで、大幅な改善ができました。
おそらく処理詰まりが解消されたためかと思われます。

もし並列化をしても思ったより改善しない場合は、極端に重い処理の見直しをすると良いかもしれません!

または、paralell_testsには「runtime log」というものがあり、
これを残すことで、次回のテストの振り分けを実行時間がなるべく均等になるようにしてくれる機能もあるようです。
そのため、そちらを試してみても良いかもしれません!

今回の改善を通してわかった、今後RSpec記述時に気をつけたいこと

最後に、今回の対応を通して今後気をつけたいと感じたことを書いていきたいと思います。

テストデータは最小限にしよう。

DRYに書くためなど、さまざまな理由でロジックに直接関係のないテストデータを用意したくなることもあるかと思います。
しかし、そのようなコードが大量に生成されてから後々削ろうと思うと、
1つ1つの効果が薄くて速度改善に時間がかかったり、FactoryBotコールバックの場合は修正範囲が大きくなってしまったりする可能性も考えられます。
そのため、直接ロジックに関係ないテストデータを作成することは安易にせず、よく考えてから行った方が良いかと感じました。

特に、最上位階層でlet!を使用していたり、FactoryBotのコールバックでデータ生成している場合は要注意かなと感じました。

大量件数での"create_list"はやめよう(大量件数では"create_build + import"を使おう)。

create_listで大量のテストデータを作成すると、件数分のクエリが発行されてしまってデータ生成時間が少し多くなるため、
大量データ生成では、create_build + importを使った方が良いと感じました。

テスト観点を分け、観点に関係ない部分で時間がかかる処理がないか考えよう。

重たい処理におけるバリデーションのテストなど、観点に関係ない部分で時間がかかる可能性を考えて、もし存在する場合は、観点外の部分をモックにすることを積極的に検討してみても良いかと感じました。

今後の展望

今回大幅に実行時間を短縮できたRSpecですが、
まだ短縮の余地はあるので更なる短縮に努めたいと思っております!

また今後機能拡張と共にRSpecを書いていく中で、書き方によっては実行時間が再度増大することも予想されます。
レビューやチームへの知見展開でもカバーできますが、
より安定した維持を実現するためには、静的解析などの「人依存」でない方法を取れると良いかと思っております。
そのため次は、短縮した実行時間をより簡単に維持できる方法を模索していきたいと思っております!

おわりに

ここまで読んでいただきありがとうございました!
この記事が皆さまの参考になれば幸いです!
また更なる進展がありましたら、ご紹介できればと思っております!
それでは!