hiyoko-programingの日記

プログラミングを勉強したてのひよっ子。   エンジニア目指して勉強中。

単体テストの応用〜factory_bot〜

factory_bot

簡単にダミーのインスタンスを作成することができるGem。他のファイルで予め各クラスのインスタンスに定めるプロパティを設定しておき、specファイルからメソッドを利用してその通りのインスタンスを作成する。factory_botを利用すれば、user_spec.rbは以下のように短い記述にすることができる。

【例】factory_botを利用した場合

user_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
require 'rails_helper'

describe User do
  describe '#create' do

    it "is invalid without a nickname" do
      user = build(:user, nickname: nil)
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end

    it "is invalid without a email" do
      user = build(:user, email: nil)
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

  end
end

Userクラスのインスタンスを作成している部分が随分短く記述されていることがわかる。では早速、factory_botを利用してみる。

 以下の指示に従ってGemfileを編集

rspecと同じグループの中に追記する。

Gemfile
1
2
3
4
5
group :development, :test do
  #省略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

その後、bundle installを行う。

続いて、specディレクトリ直下に「factories」というディレクトリを作成。その中に、作成したインスタンスの複数形のファイル名でRubyのファイルを作成する。今回の場合は、users.rbである。

 specディレクトリ直下に「factories」というディレクトリを追加し、その中に「users.rb」という名前でファイルを作成。

  •  pictweet
    •  app
    • 省略・・・
    •  spec
      •  models
        •  specファイル
      •  controllers
        •  specファイル
      •  factories
        •  users.rb

続いてusers.rbを編集する。

users.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FactoryBot.define do

  factory :user do
    nickname              {"abe"}
    email                 {"kkk@gmail.com"}
    password              {"00000000"}
    password_confirmation {"00000000"}
  end

end

これで準備は完了。このようにすると、specファイルの中で特定のメソッドにより簡単にインスタンスを生成したり、DBに保存したりできるようになる。

続いて、factory_botにおける最も基本的なメソッドであるbuildメソッドとcreateメソッドについて。

 buildメソッド

引数にシンボル型で取ったクラス名のインスタンスを、factory_botの記述をもとに作成する。例えば前述のusers.rbが存在する場合、下記2つの変数userの値は同じ値になる。

factory_botによるインスタンスの生成
1
2
3
4
#factory_botを利用しない場合
user = User.new(nickname: "abe", email: "kkk@gmail.com", password: "00000000", password_confirmation: "00000000")
#factory_botを利用する場合
user = FactoryBot.build(:user)

 createメソッド

buildとほぼ同じ働きをするが、createの場合はテスト用のDBに値が保存される。

注意すべき点として、1回のテストが実行され、終了する毎にテスト用のDBの内容がロールバックされる。(保存された値がすべて消去されてしまう)
従って、binding.pry等でテストの実行を一時停止しないとテスト用のDBに保存された値をSequel Pro等で確認することはできない。

factory_botによるインスタンスの生成
1
2
#createしたインスタンスはDBに保存される
user = FactoryBot.create(:user)

factory_botの記法の省略

factory_botによってインスタンスを作成する際に、レシーバーであるクラスのFactoryBotという記述を省略することができる。そのためには、spec/rails_helper.rbを以下のように編集する。

rails_helper.rb
1
2
3
4
5
6
7
8
#省略
RSpec.configure do |config|
  #下記の記述を追加
  config.include FactoryBot::Syntax::Methods

  #省略

end

すると、先ほどのuser_spec.rbの記述はFactoryBotを省いて以下のように省略できる。

user_spec.rb
1
2
3
4
5
6
  #nicknameが空では登録できないこと
  it "is invalid without a nickname" do
    user = build(:user, nickname: "")
    user.valid?
    expect(user.errors[:nickname]).to include("can't be blank")
  end

セットした値の上書き

factoriesディレクトリ内のファイルで予めセットした値を変更しインスタンスを生成したい場合は、specファイル側で引数を増やし、上書きすることができる。
引数はカラム名: 値という形のハッシュで、いくつでも追加することができる。

セットした値を変更する
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#デフォルトの場合
user = build(:user)
=><User:0x007fcabab94650
 id: nil,
#中略
nickname: "abe">
user = build(:user, nickname: "shinbo")
=><User:0x007fcac2a88998
 id: nil,
#中略
nickname: "shinbo">

Userクラスのインスタンスをfactory_botを利用した生成に書き直す

 問題1:user_spec.rbの中で生成しているUserクラスのインスタンスをfactory_botを利用した生成に書き直す。

作業ファイル:app/spec/models/user_spec.rb
ヒント:Userクラスを利用して作成している部分を、factory_botを利用したものに書き換える
app/spec/models/user_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
require 'rails_helper'

describe User do
  describe '#create' do

    it "is invalid without a nickname" do
      user = build(:user, nickname: "")
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end

    it "is invalid without an email" do
      user = build(:user, email: "")
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

  end
end

解説

これまでUser.new()としていた部分をbuild(:user)とすると、同様にUserクラスのインスタンスを作成できるようになっている。

buildメソッドの引数にはシンボル型で作成したいクラスの名前を渡すが、第二引数以降カラム名: 値という形式でハッシュを渡すことで、生成されるインスタンスのプロパティ値を上書きすることができる。 

テストコードを書く際の原則について

テストコードを書く際の原則

テストコードを書くにあたっては、守るべき以下の原則がある。これらはとても大切なので、必ず覚えておく。

①各exampleで期待する値は1つ
②期待する結果をはっきりわかりやすく記述
③起きて欲しいことと起きてほしくないこと両方をテストする
④境界値をテストする
⑤可読性を考えつつ、適度にDRYにする

 

①各exampleで期待する値は1つ

テストコードにおいては、example(it "hoge" do ~ end のまとまり)ひとつに必ずエクスペクテーション(expext(◯◯).to ~)をひとつ含める。
2つ以上含めてしまうと、どちらのエクスペクテーションでエラーが出たのか判別できず、正確なテストができないため。

②期待する結果をはっきりわかりやすく記述する

it "〜" do"〜"の部分は、期待する結果を書いておく場所。
明快な書き方をすることで、自身の確認やチームメンバーとの共有、顧客への仕様説明が楽になり、コミュニケーションミスも減る。

③起きて欲しいことと起きてほしくないこと両方をテストする

起きて欲しいことをチェックしたら、起きてほしくない場合にどんな結果が起こるかも想定しその通りになるか確かめる。
予期せぬ動作が残るのを防ぐため。

④境界値をテストする

6文字以上でバリデーションに引っかかる、という条件の場合は「5文字までは正常」と「6文字以上ならば異常」を確かめる。こちらも、予期せぬ動作を防ぐため。

⑤可読性を考えつつ、適度にDRYにする

DRYとは「Don't Repeat Yourself」の略で、何度も同じことを記述せず効率的にコードを書こう、という原則を意味する。しかし、テストコードにおいては何よりもわかりやすさを優先する。その結果、たとえDRYに添えなくなったとしても、わかりづらくなってテストの見落としが起きるよりはましだからである。

全ての条件を網羅したバリデーションテストを書く

では、前述したテストコードの原則に沿って、Pictweetのユーザー新規登録時のバリデーションに関するテストコードを完成させる。

テストすべき項目一覧

今回、ユーザーの登録時に確認が必要なバリデーションのテストは以下。

  1. nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
  2. nicknameが空では登録できないこと
  3. emailが空では登録できないこと
  4. passwordが空では登録できないこと
  5. passwordが存在してもpassword_confirmationが空では登録できないこと
  6. nicknameが7文字以上であれば登録できないこと
  7. nicknameが6文字以下では登録できること
  8. 重複したemailが存在する場合登録できないこと
  9. passwordが6文字以上であれば登録できること
  10. passwordが5文字以下であれば登録できないこと

このうち、いくつかのものは少し工夫が必要である。

nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること

まずは全ての条件が整っている場合に「登録ができること」を確かめるパターン。この場合は、「正常に保存されることを期待する」エクスペクテーションが必要。この時利用するマッチャがbe_validマッチャである。

 be_validマッチャ

expectの引数にしたインスタンスが全てのバリデーションをクリアする場合にパスするマッチャである。

be_validマッチャを利用すると、この条件のexampleは以下のように簡単に書くことができる。

user_spec.rb
1
2
3
4
    it "is valid with a nickname, email, password, password_confirmation" do
      user = build(:user)
      expect(user).to be_valid
    end

重複したemailが存在する場合登録できないこと

続いて、重複に関するパターン。先にユーザーを1人登録しておき、その後emailに同じ値を持つ別のユーザーが登録できるかチェックすることで確かめられる。
この時含まれるエラー文は、「has already been taken」。

user_spec.rb
1
2
3
4
5
6
7
8
    it "is invalid with a duplicate email address" do
      #はじめにユーザーを登録
      user = create(:user)
      #先に登録したユーザーと同じemailの値を持つユーザーのインスタンスを作成
      another_user = build(:user)
      another_user.valid?
      expect(another_user.errors[:email]).to include("has already been taken")
    end

では、これらも含めた全てのバリデーションに関するテストコードを書いてみる。

Pictweetの新規ユーザー登録時のテストコードを完成させる

今回、ユーザーの登録時に確認が必要なバリデーションのテストは以下。

テストすべき項目一覧

  • nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
  • nicknameが空では登録できないこと
  • emailが空では登録できないこと
  • passwordが空では登録できないこと
  • passwordが存在してもpassword_confirmationが空では登録できないこと
  • nicknameが7文字以上であれば登録できないこと
  • nicknameが6文字以下では登録できること
  • 重複したemailが存在する場合登録できないこと
  • passwordが6文字以上であれば登録できること
  • passwordが5文字以下であれば登録できないこと

 問題2:Pictweetの新規ユーザー登録時のバリデーションに関するテストコードを完成させる。

作業ファイル:app/spec/models/user_spec.rb
ヒント1:テストすべき項目一覧を確認しつつ全てのテストコードを書いていく
ヒント2:「何文字以内」もしくは「何文字以上」という条件のバリデーションでどのようなエラー文が出るか、binding.pryを利用して試してみる
app/spec/models/user_spec.rb
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
require 'rails_helper'

describe User do
  describe '#create' do
    # 1. nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
    it "is valid with a nickname, email, password, password_confirmation" do
      user = build(:user)
      expect(user).to be_valid
    end

    # 2. nicknameが空では登録できないこと
    it "is invalid without a nickname" do
      user = build(:user, nickname: nil)
      user.valid?
      expect(user.errors[:nickname]).to include("can't be blank")
    end

    # 3. emailが空では登録できないこと
    it "is invalid without a email" do
      user = build(:user, email: nil)
      user.valid?
      expect(user.errors[:email]).to include("can't be blank")
    end

    # 4. passwordが空では登録できないこと
    it "is invalid without a password" do
      user = build(:user, password: nil)
      user.valid?
      expect(user.errors[:password]).to include("can't be blank")
    end

    # 5. passwordが存在してもpassword_confirmationが空では登録できないこと
    it "is invalid without a password_confirmation although with a password" do
      user = build(:user, password_confirmation: "")
      user.valid?
      expect(user.errors[:password_confirmation]).to include("doesn't match Password")
    end

    # 6. nicknameが7文字以上であれば登録できないこと
    it "is invalid with a nickname that has more than 7 characters " do
      user = build(:user, nickname: "aaaaaaaa")
      user.valid?
      expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
    end

    # 7. nicknameが6文字以下では登録できること
    it "is valid with a nickname that has less than 6 characters " do
      user = build(:user, nickname: "aaaaaa")
      expect(user).to be_valid
    end

    # 8. 重複したemailが存在する場合登録できないこと
    it "is invalid with a duplicate email address" do
      user = create(:user)
      another_user = build(:user, email: user.email)
      another_user.valid?
      expect(another_user.errors[:email]).to include("has already been taken")
    end

    # 9. passwordが6文字以上であれば登録できること
    it "is valid with a password that has more than 6 characters " do
      user = build(:user, password: "000000", password_confirmation: "000000")
      user.valid?
      expect(user).to be_valid
    end

    # 10. passwordが5文字以下であれば登録できないこと
    it "is invalid with a password that has less than 5 characters " do
      user = build(:user, password: "00000", password_confirmation: "00000")
      user.valid?
      expect(user.errors[:password]).to include("is too short (minimum is 6 characters)")
    end
  end
end

解説

改めて、今回テストすべき項目を列挙してみます。

  1. nicknameとemail、passwordとpassword_confirmationが存在すれば登録できること
  2. nicknameが空では登録できないこと
  3. emailが空では登録できないこと
  4. passwordが空では登録できないこと
  5. passwordが存在してもpassword_confirmationが空では登録できないこと
  6. nicknameが7文字以上であれば登録できないこと
  7. nicknameが6文字以下では登録できること
  8. 重複したemailが存在する場合登録できないこと
  9. passwordが6文字以上であれば登録できること
  10. passwordが5文字以下であれば登録できないこと

このうち、「1,2,3,8」に関しては既にコードを書いている。また、「4, 5, 7, 9」に関してはnicknameやemailに関してのテストコードとほぼ同じである。

ここでは、「6」を例にあげて解説をしていく。
「nicknameが7文字以上であれば登録できないこと」を確かめるためには、以下のようなコードを記載する。

 

*【例】6に関するコードに

user_spec.rb
1
2
3
4
5
    it "is invalid with a nickname that has more than 7 characters " do
      user = build(:user, nickname: "aaaaaaaa")
      user.valid?
      expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
    end

上記の4行目で、文字数の長さに関するエラー文「"is too long (maximum is 6 characters)"」を確かめている。
そのエラー文の中身を確かめる場合は、bundle exec rspecコマンドを打つ。

user_spec.rb
1
2
3
4
5
6
    it "is invalid with a nickname that has more than 7 characters " do
      user = build(:user, nickname: "aaaaaaaa")
      user.valid?
      binding.pry
      expect(user.errors[:nickname]).to include("is too long (maximum is 6 characters)")
    end
user.errorsの中身
1
2
 pry(#<RSpec::ExampleGroups::User::Create>)> user.errors[:nickname]
=> ["is too long (maximum is 6 characters)"]

このようにエラー文の中身を判別することができるので、その後にincludeマッチャを用いて検証を行う。
これと同様の方法で、10の「passwordが5文字以下であれば登録できないこと」も確かめることができる。

なお、今回binding.pryで出力した["is too long (maximum is 6 characters)"]は配列に文字列が格納されている。このような場合でもincludeマッチャを用いることはできる。より深く知りたいなら、以下の資料を参照すると良い。

Project: RSpec Expectations 3-9

https://relishapp.com/rspec/rspec-expectations/v/3-9/docs/built-in-matchers/include-matcher#array-usage

 

factory_bot参考リンク