😺

RSpecでRailsをテストする

6 min read

『Ruby on Rails 5の上手な使い方』の第6章にRSpecの書き方が紹介されていたので、内容をまとめます。

Railsのプロジェクトを作成した直後の前提で進めます。

事前準備

Gemfileに以下を追記して、 bundle install を実行します。

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'database_cleaner'
  gem 'faker'
  gem 'pry-rails'
  gem 'pry-coolline', github: 'owst/pry-coolline', branch: 'support_new_pry_config_api'
end

RSpecの実行に必要なファイル群を生成します。

% bin/rails g rspec:install
% bundle exec rspec

DatabaseCleanerを設定する

spec/rails_helper.rb
RSpec.configure do |config|

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:trunsaction)
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

FactoryBotを設定する

spec/rails_helper.rbの`RSpec.configureブロック内の最後に以下を追記します。

spec/rails_helper.rb
config.include FactoryBot::Syntax::Methods

テスト対象のモデルを書く

% bin/rails g model Article title:text body:text status:integer published_at:datetime
% bundle exec rake db:migrate RAILS_ENV=test
app/models/article.rb
class Article < ApplicationRecord
  enum status: { draft: 0, published: 1 }

  def abbreviated_title
    title.size >= 20 ? "#{title.slice(0, 19)}..." : title
  end

  def publish
    return if self.published?
    # https://railsguides.jp/active_record_basics.html#update
    update({status: Article.statuses['published'], published_at: Time.current})
  end
end

describeメソッドでテスト対象を明らかにする

spec/models/article_spec.rb
require 'rails_helper'

RSpec.describe Article, type: :model do
  describe '.abbreviated_title' do
    it '記事タイトルがそのまま返ること' do
      article = Article.new(title: 'タイトルです')
      expect(article.abbreviated_title).to eq 'タイトルです'
    end
  end
  
  describe '.publish' do
    it '記事が公開状態になること' do
      article = Article.new(status: :draft)
      expect(article.draft?).to be_truthy
      article.publish
      expect(article.published?).to be_truthy
    end
  end
end

describeでメソッド名を指定しています。
メソッド名の前についている.ですが、Rubyではクラスメソッドの名前には.、インスタンスメソッドの名前には#を使うというものがあるためです。

.#を付けるかどうかはプロジェクトの命名規則によります。

it メソッドはテストケース自体を定義するメソッドです。it で定義されたものは example(サンプル)と呼ばれます。

contextメソッドで状況を明確にする

contextメソッドを使ってサンプルを実行する状況について説明します。
contextメソッドは同じ状況のサンプルをグループ化できます。

spec/article_spec.rb
require 'rails_helper'

RSpec.describe Article, type: :model do
  describe '.abbreviated_title' do
    context '記事タイトルが20字未満の場合' do
      it '記事タイトルがそのまま返ること' do
        article = Article.new(title: 'タイトルです')
        expect(article.abbreviated_title).to eq 'タイトルです'
      end
    end
    
    context '記事タイトルが20字以上の場合' do
      it 'タイトルが省略されること' do
        article = Article.new(title: '20字以上のタイトルは長いぜえ。省略されるぜえ')
        expect(article.abbreviated_title).to eq '20字以上のタイトルは長いぜえ。省略さ...'
      end
    end
  
  end
  
  describe '.publish' do
    
    context '記事が非公開状態の場合' do
      it '記事が公開状態になること' do
        article = Article.new(status: :draft)
        expect(article.draft?).to be_truthy
        article.publish
        expect(article.published?).to be_truthy
      end
    end
    
    context '記事が公開状態の場合' do
      it '記事が公開状態のままであること' do
        article = Article.new(status: :published)
        expect(article.published?).to be_truthy
        article.publish
        expect(article.published?).to be_truthy
      end
    end
    
  end
end

使用頻度の高いmatcher

メソッド 概要 使い方
to 〜であること expect(1 + 2).to eq 3
not_to / to_not 〜ではないこと expect(1 + 2).not_to eq 1
eq 期待値と実際の値が等しいかどうか expect(1 + 2).to eq 3
be 値の大小を比較する expect(1 + 2).to be >= 3
be_xxxx 戻り値がtrue/falseになるメソッドを検証する expect(user).to be_valid
be_truthy / be_falsey 戻り値としてtrue/falseを返すメソッドを検証する expect(user.save).to be_truthy
change ブロックを指定した後に、指定の値が何から何へ変化するかを検証する expect{ x.pop }.to change{ x.size }.by(-1)

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」

事前処理、事後処理ができるフック

フック 振る舞い
before サンプル実行前に実行される
after サンプル実行後に実行される
around ブロックの中でサンプルの実行のタイミングをん含めて定義し、実行される
フックスコープ 振る舞い
each, example exampleごとに実行される
all, context describe メソッド、 context メソッドのグループごとに実行される
suite RSpec 実行時に 1度だけ実行される。 rails_helper.rb や spec_helper.rb などのコンフィグファイルの中で定義する

pendingとskip

pending メソッドは「どうしても落ちてしまうテストがあるから保留」というときに使います。
skip メソッドはコールされたところで処理が中断され、結果に保留の「*」が表示されます。

遅延評価の let を使ってみる

遅延評価とは、実行されたときに初めて評価されるという仕組みです。
let は必要になる瞬間まで呼び出されません。

インスタンス変数代わりに let を使うケースが多いでしょう。

spec/models/article_spec.rb
require 'rails_helper'

RSpec.describe Article, type: :model do
  describe '.abbreviated_title' do
    let(:article) { Article.new(title: title) }
    context '記事タイトルが20字未満の場合' do
      let(:title) { 'タイトルです' }
      it '記事タイトルがそのまま返ること' do
        expect(article.abbreviated_title).to eq 'タイトルです'
      end
    end
    
    context '記事タイトルが20字以上の場合' do
      let(:title) { '20字以上のタイトルは長いぜえ。省略されるぜえ' }
      it 'タイトルが省略されること' do
        expect(article.abbreviated_title).to eq '20字以上のタイトルは長いぜえ。省略さ...'
      end
    end
  end
end

使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」

subject でテスト対象を明確にする

subject というメソッドを使えば、expect メソッドで指定していたテスト対象を省略することができます。

spec/models/article_spec.rb
    let(:article) { Article.new(title: title) }
    subject { article.abbreviated_title }
    context '記事タイトルが20字未満の場合' do
      let(:title) { 'タイトルです' }
      it '記事タイトルがそのまま返ること' do
        is_expected.to eq 'タイトルです'
      end
    end
    
    context '記事タイトルが20字以上の場合' do
      let(:title) { '20字以上のタイトルは長いぜえ。省略されるぜえ' }
      it 'タイトルが省略されること' do
        is_expected.to eq '20字以上のタイトルは長いぜえ。省略さ...'
      end
    end

Discussion

ログインするとコメントできます