Userモデル
環境
Gemfile(一部抜粋)
# Gemfile
source 'https://rubygems.org'
gem 'rails', '5.1.4'
gem 'rake', '12.3.1'
group :development, :test do
gem 'rspec-rails', '~> 3.7.2'
gem "factory_bot_rails"
gem 'spring-commands-rspec'
gem 'rspec-its'
gem "database_cleaner"
gem 'pry-rails'
gem 'pry-byebug' # デバッグを実施(Ruby 2.0以降で動作する)
end
group :test do
gem 'capybara'
gem "launchy", "~> 2.4.2"
gem "selenium-webdriver", "~> 2.43.0"
gem 'shoulda-matchers',
git: 'https://github.com/thoughtbot/shoulda-matchers.git',
branch: 'rails-5'
end
バージョン
$ gem list ^rails$ factory rspec capybara shoulda
*** LOCAL GEMS ***
rails (5.1.4)
*** LOCAL GEMS ***
factory_bot (4.10.0)
factory_bot_rails (4.10.0)
*** LOCAL GEMS ***
rspec-core (3.7.1)
rspec-expectations (3.7.0)
rspec-its (1.2.0)
rspec-mocks (3.7.0)
rspec-rails (3.7.2)
rspec-support (3.7.1)
spring-commands-rspec (1.0.4)
*** LOCAL GEMS ***
capybara (3.4.1)
*** LOCAL GEMS ***
shoulda-matchers (3.1.2)
フォルダ、使用ファイル
種類 | ファイル名 |
---|---|
スペック | spec/models/user_spec.rb |
サポートモジュール | spec/support/support_module.rb |
shared_examples | spec/support/shared_examples.rb |
ファクトリ(ユーザ) | spec/support/factories/users.rb |
ファクトリ(マイクロポスト) | spec/support/factories/microposts.rb |
アウトライン作成 1/6
- subjectの定義、その有効性の確認
- 属性やメソッドの検証(respond_to)
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# サブジェクト
subject(:user) { build(:user) }
# # or
# let(:user) { build(:user) }
# subject { user }
# サブジェクトの有効性
it { should be_valid }
# 属性やメソッドの検証 (別ファイル: spec/support/shared_examples.rb )
it_behaves_like "User-model respond to attribute or method"
end
shared_examples の作成
-
属性・メソッドの検証をまとめる
shared_examples.rb# spec/support/shared_examples.rb # Userモデル # 属性・メソッドの検証 shared_examples_for "User-model respond to attribute or method" do it { should respond_to(:name) } it { should respond_to(:email) } it { should respond_to(:password) } it { should respond_to(:password_confirmation) } it { should respond_to(:authenticate) } it { should respond_to(:password_digest) } it { should respond_to(:remember_digest) } it { should respond_to(:activation_digest) } it { should respond_to(:admin) } it { should respond_to(:microposts) } it { should respond_to(:feed) } it { should respond_to(:active_relationships) } it { should respond_to(:passive_relationships) } it { should respond_to(:following) } it { should respond_to(:followers) } it { should respond_to(:follow) } it { should respond_to(:unfollow) } it { should respond_to(:following?) } end
実行結果 1/6
$ bin/rspec spec/models/user_spec.rb -e "User-model respond to attribute or method"
User
behaves like User-model respond to attribute or method
should respond to #name
should respond to #email
should respond to #password
should respond to #password_confirmation
should respond to #authenticate
should respond to #password_digest
should respond to #remember_digest
should respond to #activation_digest
should respond to #admin
should respond to #microposts
should respond to #feed
should respond to #active_relationships
should respond to #passive_relationships
should respond to #following
should respond to #followers
should respond to #follow
should respond to #unfollow
should respond to #following?
Finished in 1.46 seconds (files took 2.1 seconds to load)
18 examples, 0 failures
アウトライン作成 2/6
- バリデーション:存在性、文字数、フォーマット、一意性、大文字小文字
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# validations
describe "validations"
# 存在性 presence
describe "presence"
# 名前、メールアドレス
it "name and email should not to be empty/falsy"
# パスワード、パスワード確認
context "when password and confirmation is not present"
it "@user is inavlid"
# 文字数 characters
describe "characters"
# 名前: 最大 50 文字
context "when name is too long"
it "@user is inavlid"
# メールアドレス: 最大 255 文字
context "when email is too long"
it "@user is inavlid"
# パスワード、パスワード確認: 最小 6 文字
describe "when password is too short"
it "@user is inavlid"
# email のフォーマット
describe "email format"
# invalid なフォーマット
context "when invalid format"
it "@user is inavlid"
# valid なフォーマット
context "when valid format"
it "@user is valid"
# email 一意性 unique
describe "email uniqueness"
# email が 大文字の場合
context "when email is upcase"
# 重複(invalid)となること
it "should already taken (uniqueness case insensitive)"
# 大文字小文字が混在(before_action)
context "when mixed-case"
# 小文字でDBに保存される
it "should be saved as lower-case"
end
スペック作成 2/6
-
存在性 presence、文字数 characters に関しては、Shoulda Matchers を導入して短く。
-
email 一意性 unique に関しては、FactoryBot を使用するとエラーオブジェクト(user.errors)が得られないので使用しない書き方に。
user_spec.rb
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# (省略)
# validations
describe "validations" do
# 存在性 presence
describe "presence" do
# 名前、メールアドレス
it { should validate_presence_of :name }
it { should validate_presence_of :email }
# パスワード、パスワード確認
context "when password and confirmation is not present" do
before { user.password = user.password_confirmation = " " }
it { should_not be_valid }
end
end
# 文字数 characters
describe "characters" do
it { should validate_length_of(:name).is_at_most(50) }
it { should validate_length_of(:email).is_at_most(255) }
it { should validate_length_of(:password).is_at_least(6) }
end
# email のフォーマット
describe "email format" do
# 無効なフォーマット
context "when invalid format" do
# 無効なオブジェクト
it "should be invalid" do
invalid_addr = %w[user@foo,com user_at_foo.org example.user@foo. foo@bar_baz.com foo@bar+baz.com]
invalid_addr.each do |addr|
user.email = addr
expect(user).not_to be_valid
end
end
end
# 有効なフォーマット
context "when valid format" do
# 有効なオブジェクト
it "should be valid" do
valid_addr = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
valid_addr.each do |addr|
user.email = addr
expect(user).to be_valid
end
end
end
end
# email 一意性 unique
describe "email uniqueness" do
# 重複
context "when email is duplicate and upcase" do
it "should already taken (uniqueness case insensitive)" do
user = User.create(name: "foobar", email: "foo@bar.com", password: "foobar")
dup_user = User.new(name: user.name, email: user.email.upcase, password: user.password)
expect(dup_user).not_to be_valid
expect(dup_user.errors[:email]).to include("has already been taken")
end
end
# 大文字小文字が混在(before_action)
context "when mixed-case" do
let(:mixed_case_email) { "Foo@ExAMPle.CoM" }
# 小文字でDBに保存される
it "should be saved as lower-case" do
user.email = mixed_case_email
user.save
expect(user.reload.email).to eq mixed_case_email.downcase
end
end
end
end
end
実行結果 2/6
$ bin/rspec spec/models/user_spec.rb -e "validations"
User
validations
presence
should validate that :name cannot be empty/falsy
should validate that :email cannot be empty/falsy
when password and confirmation is not present
should not be valid
characters
should validate that the length of :name is at most 50
should validate that the length of :email is at most 255
should validate that the length of :password is at least 6
email format
when invalid format
should be invalid
when valid format
should be valid
email uniqueness
when email is duplicate and upcase
should already taken (uniqueness case insensitive)
when mixed-case
should be saved as lower-case
Finished in 2.61 seconds (files took 2.47 seconds to load)
10 examples, 0 failures
アウトライン作成 3/6
- パスワード認証 (has_secure_password, authenticate?メソッド)
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# パスワード認証 (has_secure_password)
describe "has_secure_password"
# パスワード確認が不一致
context "when mismatched confirmation"
it "@user is inavlid"
# パスワード認証 (authenticate?)
describe "authenticate? method"
# 正しいパスワード
context "with valid password"
# 認証が 成功
it "success authentication"
# 誤ったパスワード
context "with invalid password"
it "fail authentication"
end
スペック作成 3/6
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# パスワード認証 (has_secure_password)
describe "has_secure_password" do
context "when mismatched confirmation" do
before { user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
end
# パスワード認証 (authenticate?)
describe "authenticate? method" do
before { user.save }
let(:found_user) { User.find_by(email: user.email) }
context "with valid password" do
it "success authentication" do
should eq found_user.authenticate(user.password)
end
it { expect(found_user).to be_truthy }
it { expect(found_user).to be_valid }
end
context "with invalid password" do
let(:incorrect) { found_user.authenticate("aaaaaaa") }
it "fail authentication" do
should_not eq incorrect
end
it { expect(incorrect).to be_falsey }
end
end
end
実行結果 3/6
$ bin/rspec spec/models/user_spec.rb -e "authenticate? method"
User
authenticate? method
with valid password
success authentication
should be truthy
should be valid
with invalid password
fail authentication
should be falsey
Finished in 0.88853 seconds (files took 2.9 seconds to load)
5 examples, 0 failures
アウトライン作成 4/6
- マイクロポストの降順表示、ユーザ削除に依存してのdestroy
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# マイクロポスト
describe "micropost association"
# 降順に表示されること
it "order descending"
# ユーザが破棄されるとマイクロポストも破棄される
it "should destroy micropost (depend on destroy user)"
end
スペック作成 4/6
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# マイクロポスト
describe "micropost association" do
before { user.save }
# 今日の投稿/昨日の投稿
let(:new_post) { create(:user_post, :today ) }
let(:old_post) { create(:user_post, :yesterday ) }
# 降順に表示されること
it "order descending" do
# インスタンス変数を呼び出さないと user.microposts.count が 0
new_post
old_post
# (セットアップの確認)
expect(user.microposts.count).to eq 2
expect(Micropost.all.count).to eq user.microposts.count
expect(user.microposts.to_a).to eq [new_post, old_post]
end
# ユーザが破棄されるとマイクロポストも破棄される
it "should destroy micropost (depend on destroy user)" do
new_post
old_post
my_posts = user.microposts.to_a
user.destroy
# ※ ユーザのマイクロポストはもとから空ではないことの確認
expect(my_posts).not_to be_empty
user.microposts.each do |post|
expect(Micropost.where(id: post.id)).to be_empty
end
end
end
end
ファクトリの作成(ユーザ/マイクロポスト)
-
before { user.save }
# spec/factories/users.rb FactoryBot.define do # 自分 # factory [任意のファクトリ名], class: [クラス名] factory :user, class: User do name "Example user" email "user@example.com" password "foobar" password_confirmation "foobar" admin false # 他人 factory :other_user do name { Faker::Name.name } email { Faker::Internet.email } end end end
trait を使用し、同じファクトリ名の特定のカラムを変更
-
let(:new_post) { create(:user_post, :today ) }
-
let(:old_post) { create(:user_post, :yesterday ) }
# spec/factories/microposts.rb FactoryBot.define do # 自分のマイクロポスト factory :user_post, class: Micropost do content { Faker::Lorem.sentence(5) } # association :[関連するモデル名], factory: :[任意につけたファクトリ名] association :user, factory: :user # or # [任意につけたファクトリ名] # user # 今日、投稿されたマイクロポスト trait :today do created_at 1.hour.ago end # 昨日、投稿されたマイクロポスト trait :yesterday do created_at 1.day.ago end end end
コンソールで確認
- ファクトリの使い方について(FactoryBot)
(抜粋)
$ rails console test --sandbox
[2] pry(main)> my_post = FactoryBot.create(:user_post)
=> #<Micropost:0x007ff569005f08
id: 1,
content: "Ut possimus cupiditate rem assumenda.",
user_id: 5,
created_at: Tue, 31 Jul 2018 11:11:33 UTC +00:00,
updated_at: Tue, 31 Jul 2018 11:11:33 UTC +00:00,
picture: nil>
[5] pry(main)> u1 = FactoryBot.create(:other_user)
=> #<User:0x007ff569bb2e28
id: 6,
name: "Les Wolff",
email: "monroe@blick.io",
created_at: Tue, 31 Jul 2018 11:14:20 UTC +00:00,
updated_at: Tue, 31 Jul 2018 11:14:20 UTC +00:00,
password_digest: "$2a$04$FBvog4q90YTRi60q2W99yOlN/WltencHpGonMxb9qCS4NMZggVw/K",
remember_digest: nil,
admin: false,
activation_digest: "$2a$04$7rOSnpywLU.hXGPRSaCjaOqCmfPZfRMyf2Ole43xa0xnI9zQQBhfq",
activated: false,
activated_at: nil>
実行結果 4/6 (エラー発生: email重複)
$ bin/rspec spec/models/user_spec.rb -e "micropost association"
User
micropost association
order descending (FAILED - 1)
should destroy micropost (depend on destroy user) (FAILED - 2)
Failures:
1) User micropost association order descending
Failure/Error: let(:new_post) { create(:user_post, :today ) }
ActiveRecord::RecordInvalid:
Validation failed: Email has already been taken
原因
コンソールで確認
-
before { user.save }
によってファクトリ名user
が作成された後で、new_post
が実行され、この時に再度同じファクトリを作成しようとしてしまい、email が重複してエラーになった。(抜粋) $ rails console test --sandbox [1] pry(main)> [2] pry(main)> user = FactoryBot.create(:user) id: 5, name: "Example user", email: "user@example.com", created_at: Tue, 31 Jul 2018 04:01:10 UTC +00:00, updated_at: Tue, 31 Jul 2018 04:01:10 UTC +00:00, password_digest: "$2a$04$VBfSSDbZ4C9g/JSTPcbsGuP1cZI.9jY1S7/TaQ/7IliHQm3ls2U4O", remember_digest: nil, admin: false, activation_digest: "$2a$04$oAo6RQDwYQW9pgy1.hibDeiPbSA5x69uoab7DM7dJq/A5AL/TRB/O", activated: false, activated_at: nil>
[6] pry(main)> new_post = FactoryBot.create(:user_post, :today) ActiveRecord::RecordInvalid: Validation failed: Email has already been taken from /home/vagrant/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.1.4/lib/active_record/validations.rb:78:in `raise_validation_error'
対策:テストコード側を変更
サンプルデータ has_many な関係を定義する
-
new_post
,old_post
作成時に、ファクトリ名user
("Example user")のインスタンスを明示的に指定して、1人のユーザが、複数のマイクロポストを持っている状態(has_many)を実現する。
↓今のこのコードを
user_spec.rb
# spec/models/user_spec.rb
describe "micropost association" do
before { user.save }
# 今日の投稿/昨日の投稿
let(:new_post) { create(:user_post, :today) }
let(:old_post) { create(:user_post, :yesterday) }
↓このように変更
# spec/models/user_spec.rb
describe "micropost association" do
before { user.save }
# 今日の投稿/昨日の投稿
# インスタンス変数(user)を明示的に指定し、1対多になるようにする
let(:new_post) { create(:user_post, :today, user: user) }
let(:old_post) { create(:user_post, :yesterday, user: user) }
コンソールで確認
- ファクトリ名
user
(user_id: 5
) がid: 1
,id: 2
の2つのマイクロポストを持っている
(抜粋)
$ rails console test --sandbox
$ rails c test -s
[1] pry(main)>
[2] pry(main)> user = FactoryBot.create(:user)
=> #<User:0x007f871cb1c128
id: 5,
name: "Example user",
email: "user@example.com",
[3] pry(main)>
[4] pry(main)> new_post = FactoryBot.create(:user_post, :today, user: user)
=> #<Micropost:0x007f871c78b040
id: 1,
content: "Dolor suscipit quisquam perspiciatis et.",
user_id: 5,
created_at: Tue, 07 Aug 2018 05:01:02 UTC +00:00,
updated_at: Tue, 07 Aug 2018 06:01:25 UTC +00:00,
picture: nil>
[5] pry(main)>
[6] pry(main)> old_post = FactoryBot.create(:user_post, :yesterday, user: user)
=> #<Micropost:0x007f871d628850
id: 2,
content: "Labore ducimus fuga voluptatem praesentium.",
user_id: 5,
created_at: Mon, 06 Aug 2018 06:01:02 UTC +00:00,
updated_at: Tue, 07 Aug 2018 06:01:43 UTC +00:00,
picture: nil>
[7] pry(main)>
[8] pry(main)>
[9] pry(main)> user.microposts.count
(0.3ms) SELECT COUNT(*) FROM "microposts" WHERE "microposts"."user_id" = ? [["user_id", 5]]
=> 2
[10] pry(main)> user.microposts
=> [#<Micropost:0x007f871f350ea0
id: 1,
content: "Dolor suscipit quisquam perspiciatis et.",
user_id: 5,
created_at: Tue, 07 Aug 2018 05:01:02 UTC +00:00,
updated_at: Tue, 07 Aug 2018 06:01:25 UTC +00:00,
picture: nil>,
#<Micropost:0x007f871f3566e8
id: 2,
content: "Labore ducimus fuga voluptatem praesentium.",
user_id: 5,
created_at: Mon, 06 Aug 2018 06:01:02 UTC +00:00,
updated_at: Tue, 07 Aug 2018 06:01:43 UTC +00:00,
picture: nil>]
実行結果 4/6 (修正後、成功)
$ bin/rspec spec/models/user_spec.rb -e "micropost association"
User
micropost association
order descending
should destroy micropost (depend on destroy user)
Finished in 2.92 seconds (files took 2.42 seconds to load)
2 examples, 0 failures
アウトライン作成 5/6
- フォロー/フォロー解除
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# フォロー/フォロー解除
describe "follow and unfollow"
# フォロー
describe "follow"
# 自分は他人をフォローしている(following?メソッド)
it "user is following other-user (following? method)"
# フォロー中のユーザの中に、他人が含まれている
it "user's following include other-user (follow method)"
# 他人のフォロワーの中に、自分が含まれている
it "other-user's followers include user (follow method)"
# フォロー解除
describe "unfollow"
# (follow と逆)
it "user is not following other-user (following? method)"
it "user's following does not include other-user (follow method)"
it "other-user's followers does not include user (follow method)"
end
スペック作成 5/6
user_spec.rb
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# (省略)
# フォロー/フォロー解除
describe "follow and unfollow" do
let(:following) { create_list(:other_user, 30) }
# let(:not_following) { create(:other_user) }
before do
user.save
following.each do |u|
user.follow(u) # => 自分が 30人をフォローする
u.follow(user) # => 他人の 30人にフォローされる
end
end
# フォロー
describe "follow" do
it "user is following other-user (following? method)" do
following.each do |u|
expect(user.following?(u)).to be_truthy
end
end
it "user's following include other-user (follow method)" do
following.each do |u|
expect(user.following).to include(u)
end
end
it "other-user's followers include user (follow method)" do
following.each do |u|
expect(u.followers).to include(user)
end
end
end
# フォロー解除
describe "unfollow" do
before do
following.each do |u|
user.unfollow(u) # => 自分が 30人をフォロー解除する
end
end
it "user is not following other-user (following? method)" do
following.each do |u|
expect(user.following?(u)).to be_falsey
end
end
it "user's following does not include other-user (follow method)" do
following.each do |u|
expect(user.following).not_to include(u)
end
end
it "other-user's followers does not include user (follow method)" do
following.each do |u|
expect(u.followers).not_to include(user)
end
end
end
end
end
実行結果 5/6
$ bin/rspec spec/models/user_spec.rb -e "follow and unfollow"
User
follow and unfollow
follow
user is following other-user (following? method)
user's following include other-user (follow method)
other-user's followers include user (follow method)
unfollow
user is not following other-user (following? method)
user's following does not include other-user (follow method)
other-user's followers does not include user (follow method)
Finished in 7.72 seconds (files took 2.08 seconds to load)
6 examples, 0 failures
アウトライン作成 6/6
- マイクロポストフィード
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
# マイクロポスト
describe "micropost association"
# (省略)
# マイクロポストフィード
describe "micropost feed"
# 表示が正しいこと
describe "have right microposts"
# フォロー中のユーザのマイクロポスト
it "following-user's post"
# 自分自身のマイクロポスト
it "my own post"
# 未フォローのユーザのマイクロポストは非表示
it "not have non-following-user's post"
end
サンプルデータのセットアップ
user_spec.rb
# spec/models/user_spec.rb
# マイクロポスト
describe "micropost association" do
before { user.save }
# マイクロポストフィード
describe "micropost feed" d
let(:following) { create_list(:other_user, 30) }
let(:not_following) { create(:other_user) }
# 関連付けされるファクトリを明示的に指定
before do
# 自分が10つのマイクロポストを保持
create_list(:user_post, 10, user: user)
# フォローしていないユーザが、10つのマイクロポストを保持
create_list(:other_user_post, 10, user: not_following)
following.each do |u|
user.follow(u) # => 自分が 30人をフォローする
u.follow(user) # => 他人の 30人にフォローされる
create_list(:other_user_post, 3, user: u)
# => 30人それぞれが、3つずつマイクロポストを保持
end
end
# (セットアップの確認)
it { expect(user.microposts.count).to eq 10 }
it { expect(not_following.microposts.count).to eq 10 }
# マイクロポストの合計が110
it { expect(Micropost.all.count).to eq 110 }
実行結果
$ bin/rspec spec/models/user_spec.rb -e "micropost feed"
User
micropost association
micropost feed
should eq 10
should eq 10
should eq 110
Finished in 5.89 seconds (files took 1.98 seconds to load)
3 examples, 0 failures
スペック作成 6/6
user_spec.rb
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
# (省略)
describe "micropost association" do
before { user.save }
# (省略)
# マイクロポストフィード
describe "micropost feed" do
let(:following) { create_list(:other_user, 30) }
let(:not_following) { create(:other_user) }
# 関連付けされるファクトリを明示的に指定
before do
# 自分が10つのマイクロポストを保持
create_list(:user_post, 10, user: user)
# フォローしていないユーザが、10つのマイクロポストを保持
create_list(:other_user_post, 10, user: not_following)
following.each do |u|
user.follow(u) # => 自分が 30人をフォローする
u.follow(user) # => 他人の 30人にフォローされる
create_list(:other_user_post, 3, user: u)
# => 30人それぞれが、3つずつマイクロポストを保持
end
end
# (セットアップの確認)
it { expect(user.microposts.count).to eq 10 }
it { expect(not_following.microposts.count).to eq 10 }
# マイクロポストの合計が100
it { expect(Micropost.all.count).to eq 110 }
describe "have right microposts" do
it "following-user's post" do
following.each do |u|
u.microposts.each do |post|
expect(user.feed).to include(post)
end
end
end
it "my own post" do
user.microposts.each do |post|
expect(user.feed).to include(post)
end
end
it "not have non-following-user's post" do
not_following.microposts.each do |post|
expect(user.feed).not_to include(post)
end
end
end
end
end
end
実行結果 6/6
$ bin/rspec spec/models/user_spec.rb -e "micropost feed"
User
micropost association
micropost feed
should eq 10
should eq 10
should eq 110
have right microposts
following-user's post
my own post
not have non-following-user's post
Finished in 11.51 seconds (files took 2.01 seconds to load)
6 examples, 0 failures