RSpecでFakerを使うならKernel.srandを設定しておけという話

RSpecでFaker1を使ってテストデータを用意している場合,テストデータが毎回ランダムになるゆえに,特に工夫をしないとテストを再現させることができなくなる.
パスしなかったテストを再現できないとトラブルシュートが難しくなってしまうが,それをある程度解決する方法を見つけたのでメモしておく

どんな状況か

自分が出会ったのはRailsでFactoryBotとFakerを組み合わせて使っている場合に出会った. あまりいい例ではないが,以下に具体的な状況を設定する

  • models/user.rb
class User < ApplicationRecord
  validates :name, format: /\A[a-zA-Z0-9 ]+\z/
end
  • factories/user.rb
FactoryBot.define do
  factory :user do
    name { Faker::Name.name }
  end
end
  • spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let!(:user) { FactoryBot.create(:user) }

  it "can be destroyed" do
    expect {
      user.destroy
    }.to change(User, :count).by(-1)
  end
end

Username属性はバリデーションとして正規表現で/\A[a-zA-Z0-9 ]+\z/にマッチすることと設定している.
しかしFaker::Name.nameは値に「.」を含むことがある.2
その場合にテスト中のlet!(:user) { FactoryBot.create(:user) }に失敗し,結果的にテストがパスしないことになる.
しかもその際に表示されるメッセージは以下のようになり,原因がはっきりと表示されない.

$ bin/rspec
F

Failures:

  1) User can destroy
     Failure/Error: let!(:user) { FactoryBot.create(:user) }

     ActiveRecord::RecordInvalid:
       Validation failed: Name is invalid

この例ははただの設定ミスであり,原因も単純なので「Fakerの生成した値が悪かったのかな」と推測もできるのだが,そのような状況ばかりではないだろう.

ちなみにRSpecのテストを再現させる方法というと,テストが通らなかったときと同じseed値を指定してRSpecを実行するという方法がある.
しかしこれはあくまでテストの実行順を再現させるだけであり,その他のランダム化されている箇所は再現できない.当然Fakerの生成する値も再現できない.

解決方法

Fakerはその値の生成にRandom.randKernel.randを使っているようなのでそのシード値を与えてやればよい. この方法はRSpecのドキュメントでも提案されており3,デフォルトのspec_helper.rbにもコメントアウトされた状態で書かれている.4

spec/spec_helper.rb

RSpec.configure do |config|
  :
  Kernel.srand config.seed
  :
end

Kernel.srandに整数値を渡すことでKernel.randRandom.randのシード値を設定することができる5

試してみる

実際にこの設定の動作を確認してみるために以下のようなテストコードを用意する.

  • spec/random_data_spec.rb
require 'rails_helper'

RSpec.describe 'Randomized data' do
  it 'prints some random strings' do
    5.times { puts Faker::Name.name }
  end
end

Faker::Name.nameを実行し値を出力する まずは上記設定を行なっていない状態でRSpecのseedを指定して実行する

$ bin/rspec spec/random_data_spec.rb --seed 1234

Randomized with seed 1234
Jackson Erdman DVM
Miss Angelina Nolan
Nicholaus Walker PhD
Terrence Pacocha
Camren Denesik
.

Finished in 0.54782 seconds (files took 0.50447 seconds to load)
1 example, 0 failures

Randomized with seed 1234

$ bin/rspec spec/random_data_spec.rb --seed 1234

Randomized with seed 1234
Kirsten Ondricka
Thora Jenkins
Lenny Mertz MD
John Sauer
Liliana Heathcote
.

Finished in 0.53317 seconds (files took 0.50113 seconds to load)
1 example, 0 failures

Randomized with seed 1234

当然Fakerは毎回異なる値を生成する.

それでは上記設定を行なった状態で同じように実行してみる.

$ bin/rspec spec/random_data_spec.rb --seed 1234

Randomized with seed 1234
Ian Bruen
Dallas Gutkowski
Augusta Kulas DVM
Glen Kuphal
Jamarcus Watsica
.

Finished in 0.61383 seconds (files took 0.59588 seconds to load)
1 example, 0 failures

Randomized with seed 1234

$ bin/rspec spec/random_data_spec.rb --seed 1234

Randomized with seed 1234
Ian Bruen
Dallas Gutkowski
Augusta Kulas DVM
Glen Kuphal
Jamarcus Watsica
.

Finished in 0.57579 seconds (files took 0.52938 seconds to load)
1 example, 0 failures

Randomized with seed 1234

Fakerは同じ値を生成するようになった. この状態でCIを行い,テストが通らなかったらそのときのseed値を指定すれば手元でも(Fakerやrandを使った値に関しては)再現できるようになる.

以上.

なお,ここで挙げたコードはGitHubに上げてある.
uyorum/play-ruby-on-rails

関連記事

comments powered by Disqus