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
実行して確かめていきます。
以下のように、RspecやFactoryGirlで必要なファイルを含めさまざまなファイルが作成されました。
できたファイルの確認は皆さんにお任せすることにしてrake db:migrate
でデータベースを初期化します。
この状態でRspecを実行してみます。
すでにテストもかなり作ってくれていますが、一つ一つコツコツと作っていこうと思います。
Userモデルのテスト
テスト作成の手順を確認しておきましょう。
- テストを行うクラスやメソッドの仕様を決める。
- テストファーストで仕様を実装していく。 つまり、テストを先に作り、テストが成功するよう作業を行う。
Userモデルのテストに取り掛かることにします。spec/models/user_spec.rb を編集していきます。
Userモデルの仕様
モデルのテストを作るために、Userモデルの仕様を考えてみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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行目で属性をチェックしていますが、まだバリデートしていないので当然通過できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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ファイルがすべて実行されるので、ファイル名まで指定します。
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を実行すると失敗します。
失敗です(端末では失敗にあたる部分は赤色で表示されています)が、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
を実行してみましょう。
成功しました。
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を実行します。
オールグリーン!成功です。
完成した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属性だけですが)。
テストです。
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
今回は少しテストファーストっぽさを体感できました。
次回はいよいよコントローラーのテストに挑戦します。