RSpecでよく用いられるgemについてまとめ

こんにちは。

私の会社ではテスティングフレームワークとしてRSpecが使われます。会社のリポジトリをざっと眺めてみて、よくRSpecと組み合わせて使われているgemたちをまとめてみました。

RSpec + FactoryBotは鉄板の組み合わせの印象です。shoulda-matchersもかなり有用なmatcherが使えるようになり、採用率は高い印象でした。

FactoryBotとshoulda-matchersはメンテナーもあのthoughtbot社ですし、GitHubのstarも多くかなりいいのではないでしょうか。

fakerも採用率が高く、FactoryBotの初期値として利用されているケースが多く見えます。

また、近年のwebシステムは何かしらのAPIを叩いているケースがほとんどかと思います。テストで実際にAPIを叩きに行くのはアンチパターンかと思うので、モックやスタブの用意が必要になります。

それについては個人的にはwebmockでshared_contextとしてAPIのモックを定義しておくのが良さそうに思いました。

RSpecはそれ自体がそれなりに学習コストのかかるフレームワークですが、これらのgemの機能も踏まえて良いテストコードを書けるようにしていきたいですね。

テストデータ系

  • テストデータを生成したり変更したりするタイプのgem

factory_bot_rails

機能

  • fixturesの代替機能で、テストデータ作成に用いられる
  • spec/factories配下にテストデータを記述する

  • specで見るcreate(:hoge)みたいなコードはFactoryBotの機能になります
let(:user) { create(:user) }
  • 上記の場合、spec/factories/users.rbにロジックが書いてあり、それに応じたデータが生成される
  • createが汎用的なワードなので分かりづらいが、本来はFactoryBot.create(:hoge)のように書く
  • spec/rails_helperに設定を書くことでFactoryBotのシンタックスを省略できるテクニックがある
# 本来はこう
let(:user) { FactoryBot.create(:user) }
# この設定を書くことでFactoryBotのシンタックスを省略できる
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end
  • traitの機能で特性に応じて項目をグルーピングし、文脈に応じたデータを生成することができる
# プレミアムユーザーを作成する
let(:user) { create(:user, :premium) }
factory :user do
  birth_year { '1990' }

  trait :premium do
    is_premium { true }
    premium_registered_at { Time.zone.now }
  end
end

補足

  • fixturesと異なる点は以下があります
    • Rubyファイルに記述する
    • spec側で都度テストデータ投入処理を書く必要がある
      • spec側で引数を渡すなどして動的にテストデータを変更できる
    • モデルのvalidationを通すので正しいデータを作成できる
  • factory_bot_railsfactory_botRailsで使用する際に用いられるgemです
    • 以前はfactory_girlという名前だった
  • thoughtbot社が開発している(大きめの会社がメンテナーなので割と安心)
  • Everyday Rails - RSpecによるRailsテスト入門 でも紹介されている

faker

機能

  • ダミーデータを生成してくれる
  • カバーするデータの範囲は幅広く、住所やメアドをはじめとし、アニメキャラの名前など色々ある
  • 日本語のロケールファイルもある

  • FactoryBotと組み合わせて用いられることが多いイメージ
FactoryBot.define do
  factory :user do
    name { Faker::Name.last_name }
    email { Faker::Internet.free_email }
    password { Faker::Internet.password(8) }
  end
end

補足

  • RSpecに限らず使用可能
  • seedにもよく使われる
  • 1万行あるcsvを生成するスクリプトとかにも使える

timecop

機能

  • 「time trave」と「time freezing」機能を提供する
  • 要は時間を進めたり止めたりして、時刻に依存したテストができるようになる

補足

  • Timecop.returnを忘れると他のテストケースにも影響がある
  • 以下のパターンで対処するのが良さそう

  • spec_helperに書いておく

config.after(:each) do
  Timecop.return
end
  • aroundで書く
around(:each) do |example|
  Timecop.freeze(Time.now + 100.years)
  example.run
  Timecop.return
end
  • beforeとafterで書く
before { Timecop.freeze(Time.now + 100.years) }
after { Timecop.return }

matcher系

  • matcherを増やすタイプのgem

shoulda-matchers

機能

  • model やcontroller のspecで便利なワンライナーのmatcherが書けるようになる

RSpec.describe User, type: :model do
  describe 'relations' do
    it { is_expected.to belong_to :user_group }
  end

  describe 'validations' do
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_presence_of(:email) }
  end
end

補足

json_spec

機能

  • jsonを取り扱うためのmatcherが増える

describe User do
  let(:user){ User.create!(first_name: "Steve", last_name: "Richert") }

    it "includes the ID" do
      user.to_json.should have_json_path("id")
      user.to_json.should have_json_type(Integer).at_path("id")
    end

    it "includes friends" do
      user.to_json.should have_json_size(0).at_path("friends")

      friend = User.create!(first_name: "Catie", last_name: "Richert")
      user.friends << friend

      user.to_json.should have_json_size(1).at_path("friends")
      user.to_json.should include_json(friend.to_json)
    end
end

補足

  • メンテされているか微妙かも?
  • 似たgemにrspec-json_matcherもあります

スタブ・モック系

  • スタブやモックを返すタイプのgem

webmock

機能

  • 外部APIなどにアクセスする機能をテストする際に、APIレスポンスのMockを定義できる
  • stub_requestが出てきたらこのgemの機能です

RSpec.shared_context "stub hoge bad request" do
  before do
    api_url = 'https://hoge.com'
    stub_request(:get, "#{api_url}/api/v1/fuga")
      .to_return(
        body: ''.to_json,
        status: 400
      )
  end
end
  • shared_contextで定義したり、rails_helperのconfig.before(:each)に書いているパターンが多い
  • specにいちいち書くと汚くなるから良さそう

補足

  • RSpecに限らず使えるが公式のREADMEにRSpecでの使い方も載っている

vcr

機能

  • 初回にHTTPリクエストした結果を「カセット」として設定したディレクトリにモックデータとしてyamlで保存しておき、2回目以降のリクエストではそれを参照するようにする
  • モックデータがなければ、実際にAPIをコールしそのリクエスト/レスポンスをyamlに保存してくれる

require 'rubygems'
require 'test/unit'
require 'vcr'

VCR.configure do |config|
  config.cassette_library_dir = "fixtures/vcr_cassettes"
  config.hook_into :webmock
end

class VCRTest < Test::Unit::TestCase
  def test_example_dot_com
    VCR.use_cassette("synopsis") do
      response = Net::HTTP.get_response(URI('http://www.iana.org/domains/reserved'))
      assert_match /Example domains/, response.body
    end
  end
end
  • fixtures/vcr_cassettes配下にモックデータが保存される
  • use_cassetteで使用するモックを定義している
  • この例だとfixtures/vcr_cassettes/synopsis.ymlのデータを使う

補足

  • 初回はリアルなリクエストが行われるので注意
  • カセットのディレクトリにモックデータがあれば以降リクエストはモックされる
    • カセットのディレクトリがGitで管理されていればチームメンバーとスタブを共有できる
  • モックは初回に記録した以降変わらないのでAPIの仕様変更には注意が必要

その他

rspec-request_describer

機能

  • request specでdescribeに書いた内容をもとに、subject内でリクエストを発行してくれる

# subject will be `get('/users')`.
RSpec.describe 'GET /users' do
  it { is_expected.to eq(200) }
end

補足

  • いくつか縛りはあるが簡潔で読みやすいspecが書ける
  • Qiitaがわかりやすい

rspec-parameterized

機能

  • 複数の項目の組み合わせでテストを行う際に簡潔に書けるようになる

  • whereとwith_themのブロックを見かけたらこのgemの機能です
describe "plus" do
  where(:a, :b, :answer) do
    [
      [1 , 2 , 3],
      [5 , 8 , 13],
      [0 , 0 , 0]
    ]
  end

  with_them do
    it "should do additions" do
      expect(a + b).to eq answer
    end
  end

補足

  • Qiitaがわかりやすい

CI系

rspec_junit_formatter

機能

  • CircleCIなどで必要みたい