RailsチュートリアルのテストをRspecで書いてみた[第13章]

はじめに

今回はRspecの学習の一環として、RailsチュートリアルのテストをRspecで書いていきます。
至らない点があるかもしれませんが、その際はコメントにてご指摘をお願いします。

各種バージョン

Ruby 2.7.0
Rails 6.0.3.3
Rspec 3.9
Capybara 3.33.0
Factory_bot_rails 6.1.0

第13章

リスト13.7: Micropostモデルのバリデーションに対するテスト

ここでMicropostモデルのテストデータをFactoryBotを使用して作成します。

spec/factories/microposts.rb

FactoryBot.define do
  factory :micropost do
    content { "micropost test" }
    association :user
  end
end

association :userとすることで、関連付けされてあるUserオブジェクトを自動で生成してくれます!!

modelスペックは以下の通りです。

spec/models/micropost_spec.rb

require 'rails_helper'

RSpec.describe Micropost, type: :model do
# associationで関連付けしているので、Userオブジェクトを自分で作らなくていい!!
  let(:micropost) { FactoryBot.create(:micropost) }

  it "is valid with micropost's test data" do
    expect(micropost).to be_valid
  end

  it "is invalid with no user_id" do
    micropost.user_id = nil
    expect(micropost).to be_invalid
  end

  it "is invalid with no content" do
    micropost.content = " "
    expect(micropost).to be_invalid
  end

  it "is invalid with 141-letter mails" do
    micropost.content = "a" * 141
    expect(micropost).to be_invalid
  end
end

ちなみに、associationを使わないと以下のように冗長なものになります。

let(:user) { FactoryBot.create(:user) }
let(:micropost) { FactoryBot.create(:micropost, user_id: user.id) }

リスト13.14: Micropostモデルの順序付けをテストする

まずはmicropostのテストデータを追加します。

spec/factories/microposts.rb

FactoryBot.define do
  factory :micropost do
    content { "micropost test" }
    created_at { 10.minutes.ago }
    association :user

    trait :yesterday do
      content { "yesterday" }
      created_at { 1.day.ago }
    end

    trait :day_before_yesterday do
      content { "day_before_yesterday" }
      created_at { 2.days.ago }
    end

    trait :now do
      content { "now!" }
      created_at { Time.zone.now }
    end
  end
end

modelスペックは以下の通りです。

spec/models/micropost_spec.rb

require 'rails_helper'

RSpec.describe Micropost, type: :model do
・・・
  describe "Sort by latest" do
 # 評価される前にdbに保存されていないといけないので、「let!」を使用します。
    let!(:day_before_yesterday) { FactoryBot.create(:micropost, :day_before_yesterday) }
 # FactoryBot.create(:micropost, :now) を一番上に持ってくるとテストの意味がなくなるので注意です。
    let!(:now) { FactoryBot.create(:micropost, :now) }
    let!(:yesterday) { FactoryBot.create(:micropost, :yesterday) }

    it "succeeds" do
      expect(Micropost.first).to eq now
    end
  end
end

リスト13.20: dependent: :destroyのテスト

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { FactoryBot.build(:user) }
・・・
  describe "dependent: :destroy" do
    before do
      user.save
      user.microposts.create!(content: "Lorem ipsum")
    end

    it "succeeds" do
      expect do
        user.destroy
      end.to change(Micropost, :count).by(-1)
    end
  end
end

リスト13.31: Micropostsコントローラの認可テスト

spec/requests/micropost_request_spec.rb

require 'rails_helper'

RSpec.describe "Microposts", type: :request do
  describe "Microposts#create" do
    let(:micropost) { FactoryBot.attributes_for(:micropost) }
    let(:post_request) { post microposts_path, params: { micropost: micropost } }

    context "when not logged in" do
      it "doesn't change Micropost's count" do
        expect { post_request }.to change(Micropost, :count).by(0)
      end

      it "redirects to login_url" do
        expect(post_request).to redirect_to login_url
      end
    end
  end

  describe "Microposts#destroy" do
    let!(:micropost) { FactoryBot.create(:micropost) }
    let(:delete_request) { delete micropost_path(micropost) }

    context "when not logged in" do
      it "doesn't change Micropost's count" do
        expect { delete_request }.to change(Micropost, :count).by(0)
      end

      it "redirects to login_url" do
        expect(delete_request).to redirect_to login_url
      end
    end
  end
end

リスト13.55: 間違ったユーザによるマイクロポスト削除に対してテストする

spec/requests/micropost_request_spec.rb

require 'rails_helper'

RSpec.describe "Microposts", type: :request do
・・・
  describe "Microposts#destroy" do
    let!(:micropost) { FactoryBot.create(:micropost) }
    let(:delete_request) { delete micropost_path(micropost) }

    context "when not logged in" do
      it "doesn't change Micropost's count" do
        expect { delete_request }.to change(Micropost, :count).by(0)
      end

      it "redirects to login_url" do
        expect(delete_request).to redirect_to login_url
      end
    end

#ここからが今回のテストです。
    context "when logged in user tyies to delete another user's micropost" do
      let(:user) { FactoryBot.create(:user) }

      before { log_in_as(user) }

      it "doesn't change Micropost's count" do
        expect { delete_request }.to change(Micropost, :count).by(0)
      end
      it "redirects to root_url" do
        expect(delete_request).to redirect_to root_url
      end
    end
  end
end

リスト13.56: マイクロポストのUIに対するテスト

systemスペックで書いています。

spec/system/micropost_interface_spec.rb

require 'rails_helper'

RSpec.describe "MicropostsInterfaces", type: :system do
  let(:user) { FactoryBot.create(:user) }
  let(:micropost) { FactoryBot.create(:micropost) }

  before do
    34.times do
      content = Faker::Lorem.sentence(word_count: 5)
      user.microposts.create!(content: content)
    end
  end

  scenario "micropost interface" do
    login_as(user)
    click_on "Home"

    # 無効な送信
    click_on "Post"
    expect(has_css?('.alert-danger')).to be_truthy

    # 正しいページネーションリンク
    click_on "2"
    expect(URI.parse(current_url).query).to eq "page=2"

    # 有効な送信
    valid_content = "This micropost really ties the room together"
    fill_in "micropost_content", with: valid_content
    expect do
      click_on "Post"
      expect(current_path).to eq root_path
      expect(has_css?('.alert-success')).to be_truthy
    end.to change(Micropost, :count).by(1)

    # 投稿を削除する
    expect do
      page.accept_confirm do
        all('ol li')[0].click_on "delete"
      end
      expect(current_path).to eq root_path
      expect(has_css?('.alert-success')).to be_truthy
    end.to change(Micropost, :count).by(-1)

    # 違うユーザのプロフィールにアクセス(削除リンクがないことを確認)
    visit user_path(micropost.user)
    expect(page).not_to have_link "delete"
  end
end

ページネーションのテストがある為、予め34個のマイクロポストを作成しています。

 before do
    34.times do
      content = Faker::Lorem.sentence(word_count: 5)
      user.microposts.create!(content: content)
    end
  end

login_as(user)は自作のヘルパーメソッドです。

spec/support/test_helper.rb

module SystemHelper
  def login_as(user)
    visit login_path
    fill_in 'Email',    with: user.email
    fill_in 'Password', with: user.password
    click_button 'Log in'
  end
end

RSpec.configure do |config|
  config.include SystemHelper
end

「正しいページネーションリンク」では以下を参考にしました。

capybaraでcurrent_pathが一致しているかどうか調べるのにハマった - Qiita

# current_path => クエリを取得しないので注意!!
URI.parse(current_url).query

「投稿を削除する」では以下を参考にしました。

【Rails】Selenium/RSpecでconfirmダイアログのテストをする - Qiita

使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita

all('ol li')[0].click_on "delete"
# allメソッドで条件(ol要素内のli要素)に合致した要素の配列が返ってくる。

また、マイクロポストの数が減っているかを確認するテストですが、以下のようにするとテストは通りません。

    expect do
      page.accept_confirm do
        all('ol li')[0].click_on "delete" 
      end
    end.to change(Micropost, :count).by(-1)
#=>expected `Micropost.count` to have changed by -1, but was changed by 0

これは、Ajaxの処理が完了する前にMicropost.countをチェックしているからです。
これを回避する為には一つ以上の動作を挟んであげる必要があります。

リスト13.64: 画像アップロードをテストするためのテンプレート

spec/system/micropost_interface_spec.rb

require 'rails_helper'

RSpec.describe "MicropostsInterfaces", type: :system do
  let(:user) { FactoryBot.create(:user) }
  let(:micropost) { FactoryBot.create(:micropost) }

  before do
    34.times do
      content = Faker::Lorem.sentence(word_count: 5)
      user.microposts.create!(content: content)
    end
  end

  scenario "micropost interface" do
    login_as(user)
    click_on "Home"

    # 無効な送信
    click_on "Post"
    expect(has_css?('.alert-danger')).to be_truthy

    # 正しいページネーションリンク
    click_on "2"
    expect(URI.parse(current_url).query).to eq "page=2"

    # 有効な送信
    valid_content = "This micropost really ties the room together"
    fill_in "micropost_content", with: valid_content
    # 下の一文追加
    attach_file 'micropost[image]', "#{Rails.root}/spec/fixtures/kitten.jpg"
    expect do
      click_on "Post"
      expect(current_path).to eq root_path
      expect(has_css?('.alert-success')).to be_truthy
    # 下の一文追加
      expect(page).to have_selector "img[src$='kitten.jpg']"
    end.to change(Micropost, :count).by(1)

    expect do
      page.accept_confirm do
        all('ol li')[0].click_on "delete"
      end
      expect(current_path).to eq root_path
      expect(has_css?('.alert-success')).to be_truthy
    end.to change(Micropost, :count).by(-1)

    # 違うユーザのプロフィールにアクセス(削除リンクがないことを確認)
    visit user_path(micropost.user)
    expect(page).not_to have_link "delete"
  end
end

画像追加の部分はattach_fileを使用しています。

attach_file 'micropost[image]', "#{Rails.root}/spec/fixtures/kitten.jpg"
# 第2引数のpathは適宜変更してください。

画面に反映されたかを検査する際にCSSセレクタである$=を使用しています。

img[src$='kitten.jpg']
/* src属性が'kitten.jpg'で終わるimg要素という意味です。*/