Posts Rspecでモデルのテスト
Post
Cancel

Rspecでモデルのテスト

Rspecでビヘイビア(振舞)駆動開発をしよう。でもテストの仕方がわからないとできませんね。今回は、実践的モデルのテストです。

前回「Rspecのインストールとテストの基本」をやりましたので、ご自分で試してみたい方はインストールその他をしておいてください。

今回は以下のことを行います。

  1. UserをScaffoldで作成

  2. FactoryGirlでユーザー(adminとuser)を作成

  3. Model spec で管理者と一般ユーザーを作成 テストファーストで開発します。

Userを作成

前回はモデルだけ作成しましたが、今回は、「User」全部こみの作成なので、Scaffoldを使います。

ここで実行するコマンドを一覧しておきます。

  • rails generate scaffold user name:string email:string password:string role:references
  • rake db:migrate
  • rspec spec あるいは rspec spec/models/user_spec.rb

実行して確かめていきます。

1
[rails_app]$ rails generate scaffold user name:string email:string password:string role:references

以下のように、RspecやFactoryGirlで必要なファイルを含めさまざまなファイルが作成されました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
invoke  active_record
create    db/migrate/20150530102141_create_users.rb
create    app/models/user.rb
invoke    rspec
create      spec/models/user_spec.rb
invoke      factory_girl
create        spec/factories/users.rb
        ...
invoke    rspec
create      spec/controllers/users_controller_spec.rb
create      spec/views/users/edit.html.erb_spec.rb
create      spec/views/users/index.html.erb_spec.rb
create      spec/views/users/new.html.erb_spec.rb
create      spec/views/users/show.html.erb_spec.rb
create      spec/routing/users_routing_spec.rb
invoke      rspec
create        spec/requests/users_spec.rb
invoke    helper
create      app/helpers/users_helper.rb
invoke      rspec
create        spec/helpers/users_helper_spec.rb
       ...

できたファイルの確認は皆さんにお任せすることにしてrake db:migrateでデータベースを初期化します。

1
2
3
4
5
[rails_app]$ rake db:migrate
== 20150530102141 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0041s
== 20150530102141 CreateUsers: migrated (0.0041s) =============================

この状態でRspecを実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[rails_app]$ rspec spec
**.**************...*............

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) UsersController GET #index assigns all users as @users
     # Add a hash of attributes valid for your model
     # ./spec/controllers/users_controller_spec.rb:40

               ...

  16) UsersHelper add some examples to (or delete) /home/work/rails/tdd_app/spec/helpers/users_helper_spec.rb
     # Not yet implemented
     # ./spec/helpers/users_helper_spec.rb:14

  17) User add some examples to (or delete) /home/work/rails/tdd_app/spec/models/user_spec.rb
     # Not yet implemented
     # ./spec/models/user_spec.rb:4


Finished in 0.60715 seconds (files took 1.73 seconds to load)
33 examples, 0 failures, 17 pending

すでにテストもかなり作ってくれていますが、一つ一つコツコツと作っていこうと思います。

Userモデルのテスト

テスト作成の手順を確認しておきましょう。

  1. テストを行うクラスやメソッドの仕様を決める。
  2. テストファーストで仕様を実装していく。 つまり、テストを先に作り、テストが成功するよう作業を行う。

Userモデルのテストに取り掛かることにします。spec/models/user_spec.rb を編集していきます。

Userモデルの仕様

モデルのテストを作るために、Userモデルの仕様を考えてみましょう。

File: spec/models/user_spec.rb

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

RSpec.describe User, type: :model do
  context "登録: " do
    it "管理者が登録できる。"
    it "管理者が複数登録できる。"
    it "一般ユーザが登録できる。"
    it "一般ユーザーが複数登録できる。"
  end

  context "バリデート: " do
    it "権限は必須である。"
    it "nameは必須である。"
    it "emailは必須である。"
    it "passwordは必須である。"
  end

end

バリデートには文字種・文字数制限やメールアドレスの書式などがありますが今回は省略します(付記 — その他のバリデーションとそのテストを追加しました)。

管理者の登録

では「管理者が登録できる。」テストの部分を取り上げます。

2行目は、前回作成したファクトリーの:role_adminです。3行目からUserの属性を定義し9行目で属性をチェックしていますが、まだバリデートしていないので当然通過できます。

File: spec/models/user_spec.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
it "管理者が登録できる。" do
  role_admin = FactoryGirl.create(:role_admin)
  admin = User.new(
                  name: "Admin",
                  email: "admin@example.com",
                  password: "PASSWORD",
                  role: role_admin
  )
  expect(admin).to be_valid
  admin.save

  user = User.all
  expect(user.size).to eq(1)
  expect(user[0].name).to eq("Admin")
  expect(user[0].email).to eq("admin@example.com")
  expect(user[0].password).to eq("PASSWORD")
  expect(user[0].role.role_name).to eq("admin")
end

9行目にあるマッチャーbe_validは、モデルの全てのバリデーションに合格しエラーがないことにマッチします。

10行目でデータベースに登録し、その下で値が正しく登録されたかをテストしています。

では、spec/models/user_spec.rbだけのテストをしてみます。rspecコマンドの引数にディレクトリーを与えるとそのディレクトリー以下の*_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
[rails_app]$ rspec spec/models/user_spec.rb
.*******

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User 登録:  管理者が複数登録できる。
     # Not yet implemented
     # ./spec/models/user_spec.rb:15

  2) User 登録:  一般ユーザが登録できる。
     # Not yet implemented
     # ./spec/models/user_spec.rb:16

  3) User 登録:  一般ユーザーが複数登録できる。
     # Not yet implemented
     # ./spec/models/user_spec.rb:17

  4) User バリデート:  権限は必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:21

  5) User バリデート:  nameは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:22

  6) User バリデート:  emailは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:23

  7) User バリデート:  passwordは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:24


Finished in 0.15658 seconds (files took 1.44 seconds to load)
8 examples, 0 failures, 7 pending

FactoryGirlの定義

毎回Userの属性を定義するのはたいへんなので、FactoryGirlで定義しそれを繰り返し使うようにしましょう。

spec/factories/users.rbに定義を記述します。Userクラスのファクトリーなのでファクトリー名が:userであればクラス名が省略できますが、異なるファクトリー名を使うときにはクラスの指定が必要です。

File: spec/factories/users.rb
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
FactoryGirl.define do

  factory :admin, class: User do
    name "Admin"
    email "admin@example.com"
    password "PASSWORD"
    role nil #FactoryGirl.create(:role_admin)
  end

  factory :user do
  end

end
   

ファクトリー内で(role_adminなどを)createしてしまうと DatabaseCleaner の管轄外になってしまいデータが削除されずに残ってしまいます。そこでroleは後で付け加えることにしました。

ファクトリー:adminを用いて次のテスト「管理者が複数登録できる。」を作ります。

File: spec/models/user_spec.rb
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 it "管理者が複数登録できる。" do
    role_admin = FactoryGirl.create(:role_admin)
    FactoryGirl.create(:admin, role: role_admin, name: "管理者1")
    FactoryGirl.create(:admin, role: role_admin, name: "管理者2", email: "admin2@example.com")
    FactoryGirl.create(:admin, role: role_admin, name: "管理者3", email: "admin3@example.com")

   users = User.all
   expect(users.size).to eq(3)
   expect(users[0].name).to eq("管理者1")
   expect(users[1].name).to eq("管理者2")
   expect(users[2].name).to eq("管理者3")

   expect(users[0].role.role_name).to eq("admin")
   expect(users[1].role.role_name).to eq("admin")
   expect(users[2].role.role_name).to eq("admin")
 end
  

3~5行目のように変更したい属性だけを指定することができます。それ以外の属性は定義されたものが使われます。

:userファクトリーでは、FactoryGirlのsequenceメソッドを使って登録される値が重複しないようにします。

File: spec/factories/users.rb
>
1
2
3
4
5
6
7
8
9
10
11
FactoryGirl.define do
     ...
  factory :user do
    sequence(:name) {|n| "User-#{n}"}
    sequence(:email) {|n| "user-#{n}@example.com"}
    sequence(:password) {|n| "PASSWORD_user-#{n}"}
    role nil #FactoryGirl.create(:role_user)
  end

end
     

:userファクトリーの中で使われている#{n}にはシークエンス番号が使われます。

File: 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
 it "一般ユーザが登録できる。" do
   FactoryGirl.create(:user, role: FactoryGirl.create(:role_user))

   users = User.all
   expect(users.size).to eq(1)
   expect(users[0].name).to eq("User-1")
 end

 it "一般ユーザーが複数登録できる。" do
   user_role = FactoryGirl.create(:role_user)
   FactoryGirl.create(:user, role: user_role)
   FactoryGirl.create(:user, role: user_role)
   FactoryGirl.create(:admin, role: FactoryGirl.create(:role_admin))
   FactoryGirl.create(:user, role: user_role)

   users = User.all
   expect(users.size).to eq(4)
   expect(users[0].name).to eq("User-2")
   expect(users[1].name).to eq("User-3")
   expect(users[3].name).to eq("User-4")
 end
  

6行目でシークエンス番号に「1」が代入され「User-1」となります。シークエンス番号はsequenceを含むファクトリーが呼ばれるたびに更新されるので、異なるテストで使われても継続した値となります(18行目)。したがって:adminユーザーが作られても影響を受けません(13行目、19、20行目)。

バリデートのテスト

バリデートの実装はテストファーストでいきます。現時点でのspec/models/user_spec.rbのバリデート部分です。

File: spec/models/user_spec.rb
>
1
2
3
4
5
6
7
8
9
10
11
context "バリデート: " do
  it "権限は必須である。" do
    user = FactoryGirl.build(:user, role: nil)
    expect(user).not_to be_valid
  end

  it "nameは必須である。"
  it "emailは必須である。"
  it "passwordは必須である。"
end
 

権限が必須なので「バリッドではない」と記述しました。しかしまだバリデートをしていないので、rspecを実行すると失敗します。

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
[rails_app]$ rspec spec/models/user_spec.rb
....F***

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User バリデート:  nameは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:68

  2) User バリデート:  emailは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:69

  3) User バリデート:  passwordは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:70


Failures:

  1) User バリデート:  権限は必須である。
     Failure/Error: expect(user).not_to be_valid
       expected #<User id: nil, name: "User-5", email: "user-5@example.com", password: "PASSWORD_user-5", role_id: nil, created_at: nil, updated_at: nil> not to be valid
     # ./spec/models/user_spec.rb:65:in `block (3 levels) in <top (required)>'

Finished in 0.09584 seconds (files took 1.45 seconds to load)
8 examples, 1 failure, 3 pending

Failed examples:

rspec ./spec/models/user_spec.rb:63 # User バリデート:  権限は必須である。

失敗です(端末では失敗にあたる部分は赤色で表示されています)が、role_idがnilになっていることを確認できました。では、モデルにバリデートを記述します。

File: app/models/user.rb
>
1
2
3
4
5
class User < ActiveRecord::Base
  belongs_to :role

  validates :role, presence: true
end

4行目でrole属性が空でないことを検証するpresenceオプションを指定しています。なお、2行目はUserモデルを(scaffoldで)作る時にrole: referencesとしたことでRoleモデルとの関連をRailsがつけてくれたのです。

では、rspec 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
[rails_app]$ rspec spec/models/user_spec.rb
.....***

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User バリデート:  nameは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:68

  2) User バリデート:  emailは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:69

  3) User バリデート:  passwordは必須である。
     # Not yet implemented
     # ./spec/models/user_spec.rb:70


Finished in 0.51458 seconds (files took 1.46 seconds to load)
8 examples, 0 failures, 3 pending

成功しました。

roleのエラーメッセ-ジがuser.errors[:role]で、値が空の時のメッセ-ジはI18n.t('errors.messages.blank')で取得できますので、これも使って残りを記述します。また、ユーザーのrole属性を毎回作るのは無駄なので、letで定義し、繰り返し使えるようにします。

File: 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
context "バリデート: " do
  let(:role_user) do
    FactoryGirl.create(:role_user)
  end
  it "権限は必須である。" do
    user = FactoryGirl.build(:user, role: nil)
    expect(user).not_to be_valid
    expect(user.errors[:role]).to eq([I18n.t('errors.messages.blank')])
  end

  it "nameは必須である。" do
    user = FactoryGirl.build(:user, name: nil, role: role_user)
    expect(user).not_to be_valid
    expect(user.errors[:name]).to eq([I18n.t('errors.messages.blank')])
  end
  it "emailは必須である。" do
    user = FactoryGirl.build(:user, email: nil, role: role_user)
    expect(user).not_to be_valid
    expect(user.errors[:email]).to eq([I18n.t('errors.messages.blank')])
  end
  it "passwordは必須である。" do
    user = FactoryGirl.build(:user, password: nil, role: role_user)
    expect(user).not_to be_valid
    expect(user.errors[:password]).to eq([I18n.t('errors.messages.blank')])
  end
 

このままでは失敗します。失敗を確認したらモデルを編集しましょう。

File: app/models/user.rb
>
1
2
3
4
5
6
7
8
class User < ActiveRecord::Base
  belongs_to :role

  validates :role, presence: true
  validates :name, presence: true
  validates :email, presence: true
  validates :password, presence: true
end   

rspecを実行します。

1
2
3
4
5
6
[rails_app]$ rspec spec/models/user_spec.rb
........

Finished in 0.40537 seconds (files took 1.49 seconds to load)
8 examples, 0 failures

オールグリーン!成功です。

完成したuser_spec.rb

spec/models/user_spec.rbの全コードです。

File: 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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
require 'rails_helper'

RSpec.describe User, type: :model do
  context "登録: " do
    it "管理者が登録できる。" do
      role_admin = FactoryGirl.create(:role_admin)
      admin = User.new(
                      name: "Admin",
                      email: "admin@example.com",
                      password: "PASSWORD",
                      role: role_admin
      )
      expect(admin).to be_valid
      admin.save

      user = User.all
      expect(user.size).to eq(1)
      expect(user[0].name).to eq("Admin")
      expect(user[0].email).to eq("admin@example.com")
      expect(user[0].password).to eq("PASSWORD")
      expect(user[0].role.role_name).to eq("admin")
    end

    it "管理者が複数登録できる。" do
      role_admin = FactoryGirl.create(:role_admin)
      FactoryGirl.create(:admin, role: role_admin, name: "管理者1")
      FactoryGirl.create(:admin, role: role_admin, name: "管理者2", email: "admin2@example.com")
      FactoryGirl.create(:admin, role: role_admin, name: "管理者3", email: "admin3@example.com")

      users = User.all
      expect(users.size).to eq(3)
      expect(users[0].name).to eq("管理者1")
      expect(users[1].name).to eq("管理者2")
      expect(users[2].name).to eq("管理者3")

      expect(users[0].role.role_name).to eq("admin")
      expect(users[1].role.role_name).to eq("admin")
      expect(users[2].role.role_name).to eq("admin")
    end

    it "一般ユーザが登録できる。" do
      FactoryGirl.create(:user, role: FactoryGirl.create(:role_user))

      users = User.all
      expect(users.size).to eq(1)
      expect(users[0].name).to eq("User-1")
      expect(users[0].role.role_name).to eq("general_user")
    end

    it "一般ユーザーが複数登録できる。" do
      user_role = FactoryGirl.create(:role_user)
      FactoryGirl.create(:user, role: user_role)
      FactoryGirl.create(:user, role: user_role)
      FactoryGirl.create(:admin, role: FactoryGirl.create(:role_admin))
      FactoryGirl.create(:user, role: user_role)

      users = User.all
      expect(users.size).to eq(4)
      expect(users[0].name).to eq("User-2")
      expect(users[3].name).to eq("User-4")
    end
  end

  context "バリデート: " do
    let(:role_user) do
      FactoryGirl.create(:role_user)
    end
    it "権限は必須である。" do
      user = FactoryGirl.build(:user, role: nil)
      expect(user).not_to be_valid
      expect(user.errors[:role]).to eq([I18n.t('errors.messages.blank')])
    end

    it "nameは必須である。" do
      user = FactoryGirl.build(:user, name: nil, role: role_user)
      expect(user).not_to be_valid
      expect(user.errors[:name]).to eq([I18n.t('errors.messages.blank')])
    end
    it "emailは必須である。" do
      user = FactoryGirl.build(:user, email: nil, role: role_user)
      expect(user).not_to be_valid
      expect(user.errors[:email]).to eq([I18n.t('errors.messages.blank')])
    end
    it "passwordは必須である。" do
      user = FactoryGirl.build(:user, password: nil, role: role_user)
      expect(user).not_to be_valid
      expect(user.errors[:password]).to eq([I18n.t('errors.messages.blank')])
    end

end

付記 — その他のバリデーションとそのテスト

本文では省略した文字種・文字数の制限および一意性の検証を付けたバリデーションとそのテストをおまけとして載せます(name属性だけですが)。

File: app/models/user.rb

1
   validates :name, length: {minimum: 2, maximum: 64, if: "name.present?"}, format: { with: /\A[^<>]*\z/ }, uniqueness: true, presence: true

テストです。

File: 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
it "nameは必須である。" do
  user = FactoryGirl.build(:user, name: nil, role: role_user)
  expect(user).not_to be_valid
  expect(user.errors[:name]).to eq([I18n.t('errors.messages.blank')])
end


it "nameは#{User.validators_on(:name)[0].options[:minimum]}以上である。" do
  user = FactoryGirl.build(:user, name: "a"*2, role: role_user)
  expect(user).to be_valid
  expect(user.name.length).to be >= (User.validators_on(:name)[0].options[:minimum].to_i)
  expect(user.errors[:name]).to eq([])

  user = FactoryGirl.build(:user, name: "a", role: role_user)
  expect(user).not_to be_valid
  expect(user.name.length).not_to be >= (User.validators_on(:name)[0].options[:minimum].to_i)
  expect(user.errors[:name]).to eq([I18n.t('errors.messages.too_short', {count: User.validators_on(:name)[0].options[:minimum]})])
end

it "nameは#{User.validators_on(:name)[0].options[:maximum]}以下である。" do
  user = FactoryGirl.build(:user, name: "a"*64, role: role_user)
  expect(user).to be_valid
  expect(user.name.length).to be <= (User.validators_on(:name)[0].options[:maximum].to_i)
  expect(user.errors[:name]).to eq([])

  user = FactoryGirl.build(:user, name: "a"*65, role: role_user)
  expect(user).not_to be_valid
  expect(user.name.length).not_to be <= (User.validators_on(:name)[0].options[:maximum].to_i)
  expect(user.errors[:name]).to eq([I18n.t('errors.messages.too_long', {count: User.validators_on(:name)[0].options[:maximum]})])
end


it "nameは一意である。" do
  me = FactoryGirl.create(:user, name: "My name is only one.", email: "right@me.ex")
  expect(me).to be_valid
  user = FactoryGirl.build(:user, name: "My name is only one.", email: "bad@not.me", role: role_user)

  expect(user).not_to be_valid
  expect(user.errors[:name]).to eq([I18n.t('errors.messages.taken')])
end

it "nameの書式は#{User.validators_on(:name)[1].options[:with]}である。" do
  user = FactoryGirl.build(:user, name: "a<", role: role_user)
  expect(user).not_to be_valid
  expect(user.errors[:name]).to eq([I18n.t('errors.messages.invalid')])

  user = FactoryGirl.build(:user, name: "a>", role: role_user)
  expect(user).not_to be_valid
  expect(user.errors[:name]).to eq([I18n.t('errors.messages.invalid')])
end

今回は少しテストファーストっぽさを体感できました。

次回はいよいよコントローラーのテストに挑戦します。

シェア
#内容発言者

Rspecのインストールとテストの基本

PHPの参照をめぐる冒険

坂井和郎