RSpecにおけるparameterized testingとマネーフォワードにおけるその改善

こんにちは。
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

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket