【Rails】RSpecを使った自動テスト《モデルスペック編》

はじめに

Railsでアプリケーションを開発する際、品質を担保するためにテストは欠かせません。その中でも、RSpecは多くの開発者に愛用されているテストフレームワークです。

今回は、RSpecを使ったRailsアプリケーションのテスト方法について、実践的な視点から解説していきます。

モデルスペック

バリデーションのテスト

バリデーションのテストでは、モデルが適切な条件下で正しく保存されるか、または保存されないかを確認します。以下に、典型的なバリデーションテストの例を示します。

まず、Userモデルを定義します。このモデルには名前 (name) とメールアドレス (email) が必要で、パスワード (password) の長さは最低6文字であることを検証します。

app/models/user.rb

class User < ApplicationRecord
  validates :name, presence: true
  validates :email, presence: true, uniqueness: true
  validates :password, length: { minimum: 6 }
end

次に、RSpecを使ってこのモデルのバリデーションをテストします。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  context 'validations' do
    it 'is valid with valid attributes' do
      user = User.new(name: 'Alice', email: 'alice@example.com', password: 'password')
      expect(user).to be_valid
    end

    it 'is not valid without a name' do
      user = User.new(name: nil, email: 'alice@example.com', password: 'password')
      expect(user).not_to be_valid
      expect(user.errors[:name]).to include("can't be blank")
    end

    it 'is not valid without an email' do
      user = User.new(name: 'Alice', email: nil, password: 'password')
      expect(user).not_to be_valid
      expect(user.errors[:email]).to include("can't be blank")
    end

    it 'is not valid with a duplicate email' do
      User.create!(name: 'Alice', email: 'alice@example.com', password: 'password')
      user = User.new(name: 'Bob', email: 'alice@example.com', password: 'password123')
      expect(user).not_to be_valid
      expect(user.errors[:email]).to include("has already been taken")
    end

    it 'is not valid with a short password' do
      user = User.new(name: 'Alice', email: 'alice@example.com', password: 'short')
      expect(user).not_to be_valid
      expect(user.errors[:password]).to include("is too short (minimum is 6 characters)")
    end
  end
end
  1. 有効な属性でのバリデーション (is valid with valid attributes)
    • 名前、メール、パスワードが全て有効な場合、モデルが有効であることを確認します。
  2. 名前がない場合のバリデーション (is not valid without a name)
    • 名前がnilの場合、モデルが無効であることを確認し、name属性のエラーメッセージが期待通りであることをチェックします。
  3. メールアドレスがない場合のバリデーション (is not valid without an email)
    • メールアドレスがnilの場合、モデルが無効であることを確認し、email属性のエラーメッセージが期待通りであることをチェックします。
  4. 重複するメールの場合のバリデーション (is not valid with a duplicate email)
    • 既に存在するメールアドレスを持つユーザーを作成しようとした場合、モデルが無効であることを確認し、email属性のエラーメッセージが期待通りであることをチェックします。
  5. 短いパスワードの場合のバリデーション (is not valid with a short password)
    • パスワードが6文字未満の場合、モデルが無効であることを確認し、password属性のエラーメッセージが期待通りであることをチェックします。

スコープのテスト

スコープのテストでは、モデルに定義されたスコープメソッドが期待通りの結果を返すかを確認します。スコープはActiveRecordで定義されるクエリメソッドです。

まず、Userモデルにスコープを定義します。このモデルには、activeというスコープを持ち、active属性がtrueのユーザーのみを返すようにします。

app/models/user.rb

class User < ApplicationRecord
  scope :active, -> { where(active: true) }
end

次に、RSpecを使ってこのスコープをテストします。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '.active' do
    before do
      @active_user1 = User.create!(name: 'Active User 1', active: true)
      @active_user2 = User.create!(name: 'Active User 2', active: true)
      @inactive_user = User.create!(name: 'Inactive User', active: false)
    end

    it 'includes users who are active' do
      expect(User.active).to include(@active_user1, @active_user2)
    end

    it 'does not include users who are inactive' do
      expect(User.active).not_to include(@inactive_user)
    end
  end
end
  1. beforeブロック
    • テストの前にデータベースにユーザーを作成します。@active_user1@active_user2activetrueで、@inactive_useractivefalseです。
  2. スコープにアクティブなユーザーが含まれるかのテスト (includes users who are active)
    • User.activeスコープが、@active_user1@active_user2を含むことを確認します。
  3. スコープにインアクティブなユーザーが含まれないかのテスト (does not include users who are inactive)
    • User.activeスコープが、@inactive_userを含まないことを確認します。

次に、より複雑なスコープをテストする例を示します。recentというスコープを定義し、過去7日以内に作成されたユーザーのみを返すようにします。

app/models/user.rb

class User < ApplicationRecord
  scope :recent, -> { where('created_at >= ?', 7.days.ago) }
end

このスコープのテストを以下のように行います。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '.recent' do
    before do
      @recent_user = User.create!(name: 'Recent User', created_at: 2.days.ago)
      @old_user = User.create!(name: 'Old User', created_at: 10.days.ago)
    end

    it 'includes users created within the last 7 days' do
      expect(User.recent).to include(@recent_user)
    end

    it 'does not include users created more than 7 days ago' do
      expect(User.recent).not_to include(@old_user)
    end
  end
end
  1. beforeブロック
    • テストの前にデータベースにユーザーを作成します。@recent_userは2日前に作成され、@old_userは10日前に作成されます。
  2. スコープに過去7日以内に作成されたユーザーが含まれるかのテスト (includes users created within the last 7 days)
    • User.recentスコープが、@recent_userを含むことを確認します。
  3. スコープに7日以上前に作成されたユーザーが含まれないかのテスト (does not include users created more than 7 days ago)
    • User.recentスコープが、@old_userを含まないことを確認します。

インスタンスメソッドのテスト

インスタンスメソッドのテストでは、特定のモデルインスタンスに対するメソッドが期待通りに動作するかを確認します。以下に、典型的なインスタンスメソッドのテストの例を示します。

まず、Userモデルにインスタンスメソッドを定義します。ここでは、ユーザーのフルネームを返すfull_nameメソッドを例にします。

app/models/user.rb

class User < ApplicationRecord
  def full_name
    "#{first_name} #{last_name}"
  end
end

次に、RSpecを使ってこのメソッドをテストします。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#full_name' do
    it 'returns the full name of the user' do
      user = User.new(first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end

    it 'returns the full name when either first or last name is missing' do
      user = User.new(first_name: 'John', last_name: nil)
      expect(user.full_name).to eq('John ')

      user = User.new(first_name: nil, last_name: 'Doe')
      expect(user.full_name).to eq(' Doe')
    end

    it 'returns an empty string when both first and last names are missing' do
      user = User.new(first_name: nil, last_name: nil)
      expect(user.full_name).to eq('')
    end
  end
end
  1. ユーザーのフルネームが返却されるかのテスト (returns the full name of the user)
    • ユーザーのfirst_namelast_nameが両方とも存在する場合、full_nameメソッドが正しくフルネームを返すことを確認します。
  2. 姓か名のどちらかがない場合、姓か名のどちらかが返却されるかのテスト (returns either first or last when either first or last name is missing)
    • first_nameまたはlast_nameのどちらかが存在しない場合でも、full_nameメソッドがそれに応じた結果を返すことを確認します。例えば、first_nameが存在してlast_nameが存在しない場合、full_nameは"John "を返し、逆の場合は" Doe"を返します。
  3. 姓と名の両方がない場合、空文字列が返却されるかのテスト (returns an empty string when both first and last names are missing)
    • first_namelast_nameの両方が存在しない場合、full_nameメソッドが空の文字列を返すことを確認します。

次に、より複雑なインスタンスメソッドをテストする例を示します。ここでは、ユーザーがアクティブかどうかをチェックするactive?メソッドを定義します。

app/models/user.rb

class User < ApplicationRecord
  def active?
    active && !banned
  end
end

このメソッドのテストを以下のように行います。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#active?' do
    it 'returns true if the user is active and not banned' do
      user = User.new(active: true, banned: false)
      expect(user.active?).to be true
    end

    it 'returns false if the user is not active' do
      user = User.new(active: false, banned: false)
      expect(user.active?).to be false
    end

    it 'returns false if the user is banned' do
      user = User.new(active: true, banned: true)
      expect(user.active?).to be false
    end

    it 'returns false if the user is neither active nor banned' do
      user = User.new(active: false, banned: true)
      expect(user.active?).to be false
    end
  end
end
  1. ユーザーがアクティブかつ禁止されていない場合、trueが返却されるかのテスト (returns true if the user is active and not banned)
    • ユーザーがactiveであり、かつbannedでない場合、active?メソッドがtrueを返すことを確認します。
  2. ユーザーがアクティブでない場合、falseが返却されるかのテスト (returns false if the user is not active)
    • ユーザーがactiveでない場合、active?メソッドがfalseを返すことを確認します。
  3. ユーザーが禁止されている場合、falseが返却されるかのテスト (returns false if the user is banned)
    • ユーザーがbannedである場合、active?メソッドがfalseを返すことを確認します。
  4. ユーザーがアクティブでなく禁止されている場合、falseが返却されるかのテスト (returns false if the user is neither active nor banned)
    • ユーザーがactiveでなく、かつbannedである場合、active?メソッドがfalseを返すことを確認します。

クラスメソッドのテスト

クラスメソッドのテストでは、特定のモデルクラスに対するメソッドが期待通りに動作するかを確認します。以下に、典型的なクラスメソッドのテストの例を示します。

まず、Userモデルにクラスメソッドを定義します。ここでは、メールアドレスでユーザーを検索するfind_by_emailメソッドを例にします。

app/models/user.rb

class User < ApplicationRecord
  def self.find_by_email(email)
    where(email: email).first
  end
end

次に、RSpecを使ってこのメソッドをテストします。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '.find_by_email' do
    before do
      @user1 = User.create!(name: 'Alice', email: 'alice@example.com')
      @user2 = User.create!(name: 'Bob', email: 'bob@example.com')
    end

    it 'returns the user with the given email' do
      expect(User.find_by_email('alice@example.com')).to eq(@user1)
    end

    it 'returns nil if no user with the given email exists' do
      expect(User.find_by_email('nonexistent@example.com')).to be_nil
    end
  end
end
  1. beforeブロック
    • テストの前にデータベースにユーザーを作成します。@user1alice@example.comというメールアドレスを持ち、@user2bob@example.comというメールアドレスを持ちます。
  2. 指定されたメールアドレスを持つユーザーが返却されるかのテスト (returns the user with the given email)
    • User.find_by_email('alice@example.com')@user1を返すことを確認します。
  3. 指定されたメールアドレスを持つユーザーが存在しない場合、nilが返却されるかのテスト (returns nil if no user with the given email exists)
    • 存在しないメールアドレスを検索した場合、User.find_by_email('nonexistent@example.com')nilを返すことを確認します。

次に、より複雑なクラスメソッドをテストする例を示します。ここでは、特定の期間内に作成されたユーザーを検索するcreated_withinメソッドを定義します。

app/models/user.rb

class User < ApplicationRecord
  def self.created_within(start_date, end_date)
    where(created_at: start_date..end_date)
  end
end

このメソッドのテストを以下のように行います。

spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '.created_within' do
    before do
      @user1 = User.create!(name: 'Alice', created_at: 2.days.ago)
      @user2 = User.create!(name: 'Bob', created_at: 10.days.ago)
      @user3 = User.create!(name: 'Charlie', created_at: 1.day.ago)
    end

    it 'returns users created within the given date range' do
      start_date = 3.days.ago
      end_date = 1.day.ago
      expect(User.created_within(start_date, end_date)).to include(@user1, @user3)
    end

    it 'does not return users created outside the given date range' do
      start_date = 3.days.ago
      end_date = 1.day.ago
      expect(User.created_within(start_date, end_date)).not_to include(@user2)
    end
  end
end
  1. beforeブロック
    • テストの前にデータベースにユーザーを作成します。@user1は2日前に作成され、@user2は10日前に作成され、@user3は1日前に作成されます。
  2. 指定された日付範囲内に作成されたユーザーが返却されるかのテスト (returns users created within the given date range)
    • 指定した日付範囲内で作成されたユーザー(@user1@user3)が返されることを確認します。
  3. 指定した日付範囲外に作成されたユーザーが返却されないかのテスト (does not return users created outside the given date range)
    • 指定した日付範囲外で作成されたユーザー(@user2)が返されないことを確認します。

まとめ

RSpecを使いこなすことで、Railsアプリケーションの品質を大幅に向上させることができます。ただし、テストの書きすぎには注意が必要です。重要な機能や複雑なロジックに焦点を当て、バランスの取れたテスト戦略を立てることが大切です。

また、CIツールと組み合わせることで、継続的にテストを実行し、問題を早期に発見することができます。例えば、GitHubActionsを使えば、プッシュやプルリクエスト時に自動的にテストを実行できます。

Railsアプリケーション開発において、RSpecは強力な味方となります。ぜひ、日々の開発に取り入れて、より堅牢なアプリケーション作りを目指してください。

関連記事

【Rails】RSpecを使った自動テスト《システムスペック編》
# はじめに Railsでアプリケーションを開発する際、品質を担保するためにテストは欠かせません。その中でも、RSpecは多くの開発者に愛用されているテストフレームワークです。 今回は、RSpecを使ったRailsアプリケーションのテスト方 [...]
2024年7月11日 17:07
【Rails】RSpecを使った自動テスト《リクエストスペック編》
# はじめに Railsでアプリケーションを開発する際、品質を担保するためにテストは欠かせません。その中でも、RSpecは多くの開発者に愛用されているテストフレームワークです。 今回は、RSpecを使ったRailsアプリケーションのテスト方 [...]
2024年7月11日 16:06
【Rails】RSpecを使った自動テスト《コントローラースペック編》
# はじめに Railsでアプリケーションを開発する際、品質を担保するためにテストは欠かせません。その中でも、RSpecは多くの開発者に愛用されているテストフレームワークです。 今回は、RSpecを使ったRailsアプリケーションのテスト方 [...]
2024年7月11日 15:39
【Rails】RSpecを使った自動テスト《基本編》
# はじめに Railsでアプリケーションを開発する際、品質を担保するためにテストは欠かせません。その中でも、RSpecは多くの開発者に愛用されているテストフレームワークです。 今回は、RSpecを使ったRailsアプリケーションのテスト方 [...]
2024年7月10日 17:49
【Rails】デザインパターン「Concern」の基本情報と実装方法
# はじめに Ruby on RailsなどのMVCフレームワークで構築したWebシステムにはアンチパターンというものが存在します。システム開発におけるアンチパターンとは、避けるべき悪い設計や実装方法のことを指します。 MVCフレームワーク [...]
2024年7月10日 13:32
【Rails】デザインパターン「Form Object」の基本情報と実装方法
# はじめに Ruby on RailsなどのMVCフレームワークで構築したWebシステムにはアンチパターンというものが存在します。システム開発におけるアンチパターンとは、避けるべき悪い設計や実装方法のことを指します。 MVCフレームワーク [...]
2024年7月6日 22:50
【Rails】デザインパターン「Service Object」の基本情報と実装方法
# はじめに Ruby on RailsなどのMVCフレームワークで構築したWebシステムにはアンチパターンというものが存在します。システム開発におけるアンチパターンとは、避けるべき悪い設計や実装方法のことを指します。 MVCフレームワーク [...]
2024年7月6日 19:59
【Rails】Dockerを使用してRuby on Railsの開発環境を構築
# はじめに Ruby on Railsは強力なWebアプリケーションフレームワークですが、開発環境の構築には時間がかかることがあります。特に、チーム全体で一貫した環境を維持することは困難な場合があります。 Dockerを使用することで、R [...]
2024年7月5日 18:49