Rspecでビヘイビア(振舞)駆動開発をしよう。でもテストの仕方がわからないとできませんね。今回は、実践的モデルのテストです。
前回「Rspecのインストールとテストの基本」をやりましたので、ご自分で試してみたい方はインストールその他をしておいてください。
今回は以下のことを行います。
UserをScaffoldで作成
FactoryGirlでユーザー(adminとuser)を作成
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モデルのテスト
テスト作成の手順を確認しておきましょう。
- テストを行うクラスやメソッドの仕様を決める。
- テストファーストで仕様を実装していく。 つまり、テストを先に作り、テストが成功するよう作業を行う。
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であればクラス名が省略できますが、異なるファクトリー名を使うときにはクラスの指定が必要です。
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を用いて次のテスト「管理者が複数登録できる。」を作ります。
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
メソッドを使って登録される値が重複しないようにします。
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}
にはシークエンス番号が使われます。
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のバリデート部分です。
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になっていることを確認できました。では、モデルにバリデートを記述します。
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
で定義し、繰り返し使えるようにします。
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
このままでは失敗します。失敗を確認したらモデルを編集しましょう。
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の全コードです。
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
今回は少しテストファーストっぽさを体感できました。
次回はいよいよコントローラーのテストに挑戦します。