こんにちは。 Railsエンジニアのkamillleです。
マネーフォワードのアプリケーションはユーザーの権限やプラン・リクエスト時間等によって異なる振る舞いをするものが多く、そういった機能のテストではparameterized testingを利用することが多いです。マネーフォワードのRailsアプリケーションではテスティングフレームワークとしてRSpecが採用されることが多く、RSpecにおいてどのようにparameterized testingを行っているか紹介したいと思います。
RSpecにおけるparameterized testing
RSpecはparameterized testingをサポートしていないので何かしらの拡張を行う必要があります。この拡張としてrspec-parameterizedというgemがあり、このgemが選ばれているケースが多いように思います。rspec-parameterizedは同じテストロジックに対し複数の入力値や結果を指定できるインターフェースを提供してくれるとても便利なgemです。(rspec-parameterizedの使い方はGoogle先生に聞いていただければと思います🙏)
マネーフォワードでもrspec-parameterizedには長年お世話になっていますが、使っていく間でrspec-parameterizedのデメリットが目立つケースが出てきました。
rspec-parameterizedのデメリット
rspec-parameterizedはwhereに渡すブロックをトランザクション外で評価するため、例えばtravel_toで時間を固定していてもTime.currentが現在時刻として評価されたり、ブロック内でDB操作を行っている場合はDB操作がRSpecのuse_transactional_fixturesのトランザクション外で評価されるため、あるcontextで作ったデータが別のcontextでも存在したままになる、というデメリットがあります。
ちょっと例を書いてみます。
before { travel_to Time.zone.local(2020, 1, 1, 0, 0, 0) } where :time do [ # travel_toで固定した 2020/01/01 00:00:00 になってほしいが、 # トランザクション外で評価されるので現在時刻として評価されてしまう [Time.current] ] end
# id 2のUserを作る際、データベースにid 1のUserが残ってしまう(※ use_transactional_fixtures: true な環境では) # database_cleanerやdatabase_rewinderを使っている場合は大丈夫かもしれない where :user do [ [User.create!(id: 1)], [User.create!(id: 2)] ] end
このデメリットを知っていれば気をつけた書き方をしたりレビューで指摘することができるのですが、自分の経験として知らないうちは思わぬ挙動を誘発してハマってしまったり、レビューを通り抜けてランダムフェイルの温床になってしまったケース等がありました。
このデメリットに対しalpaca-tcがメインで開発を行っているアプリケーションではトランザクション内でブロックを評価する独自の拡張を入れており、自分がメインで開発をしているアプリケーションでもこれをコピーしてきて使わせてもらっていました。rspec-parameterizedとのインターフェースの差分がほとんどなく使いやすかったためgem化しませんか?と相談したところ二つ返事でOKしてもらいgem化されました🎉
二つ返事の図(1分後にはOKが返ってきましたw) 30分後にはgem化されていた図
rspec-parameterized-contextの紹介
という背景でgem化されたのがrspec-parameterized-contextです。 whereとwith_themをparameterizedというメソッドに1つのブロックとして渡す必要がある・whereに渡すブロックで定義しているコンテキスト数をキーワード引数sizeとしてwhereメソッドに渡す必要がある、以外の点はrspec-parameterizedと同じインターフェースなので学習コストはほぼ0だと思います。
describe "Addition" do parameterized do where(:a, :b, :answer, size: 3) do [ [1, 2, 3], [5, 8, 13], [0, 0, 0] ] end with_them do it do expect(a + b).to eq answer end end end end
rspec-parameterizedのデメリットであったトランザクション外でのブロック評価もrspec-parameterized-contextならトランザクション内で評価されるようになります。
describe 'Evaluting block that given to where in transaction' do let(:now) { Date.new(2020, 1, 1) } # Travel to 2020/1/1 before { travel_to(now) } parameterized do where(:current_on, size: 1) do [ [Date.current], ] end with_them do it do # rspec-parameterized-contextの場合、current_on は 2020/1/1 として評価される # rspec-parameterizedの場合、current_on は 現在時刻 として評価される expect(current_on).to eq now end end end end
まとめ
rspec-parameterized-contextを使うことでより直感的に値が評価されるテストを書くことができるようになり、テストの可読性や認知負荷(コードを書く or レビュー時に気にしないといけない部分)を下げることができました。マネーフォワードでは自分たちの使いやすいように既存のライブラリを拡張したりプルリクエストを送る文化があり、これらによって日々の開発生産性が支えられています。
rspec-parameterized-contextにプルリクエストを送る僕の図 https://github.com/alpaca-tc/rspec-parameterized-context/pull/10
次回はまた違う拡張をご紹介できたらいいなと思っています。それでは!
--
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【サイトのご案内】 ■マネーフォワード採用サイト ■Wantedly ■京都開発拠点
【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』