💎
RSpecでパラメタライズドテストを書く
パラメタライズドテストとは
パラメタライズドテスト(Parameterized Test)は、同じテストケースを異なる入力データセットで繰り返し実行するテスト手法です。
パラメタライズドテストを使わないでテストを書いてみる
以下のモデルのバリデーションに対してテストを書いてみます。
記事を管理するArticle
モデルを作成しました。
記事は件名、本文、スラッグ、公開日時を持ちます。
以下のバリデーションを定義しました。
-
title
(件名)- 必須
- MAX255文字
-
body
(本文)- 必須
- MAX10000文字
-
slug
(スラッグ)- 必須
- MAX255文字
- ユニーク
-
published_at_must_be_in_the_future
(カスタムバリデーション)-
published_at
は未来であること
-
コードにすると以下のようになりました。
app/models/article.rb
class Article < ApplicationRecord
validates :title, presence: true, length: { maximum: 255 }
validates :body, presence: true, length: { maximum: 10000 }
validates :slug, presence: true, length: { maximum: 255 }, uniqueness: true
validate :published_at_must_be_in_the_future
private
def published_at_must_be_in_the_future
if published_at.present? && published_at <= Time.zone.now
errors.add(:published_at, "は未来の日時を指定してください")
end
end
end
テストは以下のようになりました。
spec/models/article_spec.rb
require "rails_helper"
RSpec.describe Article, type: :model do
describe "validations" do
describe "title" do
context "空でない場合" do
let(:article) { build(:article, title: "記事タイトル") }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "nilの場合" do
let(:article) { build(:article, title: nil) }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:title]).to include "を入力してください"
end
end
context "空文字の場合" do
let(:article) { build(:article, title: "") }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:title]).to include "を入力してください"
end
end
context "文字数が254文字の場合" do
let(:article) { build(:article, title: "a" * 254) }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "文字数が255文字の場合" do
let(:article) { build(:article, title: "a" * 255) }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "文字数が256文字の場合" do
let(:article) { build(:article, title: "a" * 256) }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:title]).to include "は255文字以内で入力してください"
end
end
end
describe "body" do
context "空でない場合" do
let(:article) { build(:article, body: "記事本文") }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "nilの場合" do
let(:article) { build(:article, body: nil) }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:body]).to include "を入力してください"
end
end
context "空文字の場合" do
let(:article) { build(:article, body: "") }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:body]).to include "を入力してください"
end
end
context "文字数が9999文字以内の場合" do
let(:article) { build(:article, body: "a" * 9999) }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "文字数が10000文字の場合" do
let(:article) { build(:article, body: "a" * 10000) }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "文字数が10001文字の場合" do
let(:article) { build(:article, body: "a" * 10001) }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:body]).to include "は10000文字以内で入力してください"
end
end
end
describe "slug" do
context "空でない場合" do
let(:article) { build(:article, slug: "article-slug") }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "nilの場合" do
let(:article) { build(:article, slug: nil) }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:slug]).to include "を入力してください"
end
end
context "空文字の場合" do
let(:article) { build(:article, slug: "") }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:slug]).to include "を入力してください"
end
end
context "文字数が254文字以内の場合" do
let(:article) { build(:article, slug: "a" * 254) }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "文字数が255文字の場合" do
let(:article) { build(:article, slug: "a" * 255) }
it "有効であること" do
expect(article.valid?).to be_truthy
end
end
context "文字数が256文字の場合" do
let(:article) { build(:article, slug: "a" * 256) }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:slug]).to include "は255文字以内で入力してください"
end
end
context "slugがすでに存在する場合" do
let!(:already_existing_article) { create(:article, slug: "already-existing-article") }
let(:article) { build(:article, slug: "already-existing-article") }
it "無効であること" do
expect(article.valid?).to be_falsey
expect(article.errors[:slug]).to include "はすでに存在しています"
end
end
end
describe '#published_at_must_be_in_the_future' do
context 'published_atがnilの場合' do
let(:article) { build(:article, published_at: nil) }
it '有効であること' do
expect(article.valid?).to be_truthy
end
end
context 'published_atが未来の日付の場合' do
let(:article) { build(:article, published_at: Time.zone.now + 1.day) }
it '有効であること' do
expect(article.valid?).to be_truthy
end
end
context 'published_atが現在日時の場合' do
let(:article) { build(:article) }
it '無効であること' do
freeze_time do
article.published_at = Time.zone.now
article.valid?
expect(article.errors[:published_at]).to include "は未来の日時を指定してください"
end
end
end
context 'published_atが過去の日付の場合' do
let(:article) { build(:article, published_at: Time.zone.now - 1.day) }
it '無効であること' do
article.valid?
expect(article.errors[:published_at]).to include "は未来の日時を指定してください"
end
end
end
end
end
パラメタライズドテストを使ってテストを書いてみる
RSpecでパラメタライズドテストを実現するために gem rspec-parameterized を導入します。
Gemfile
gem 'rspec-parameterized'
READMEを見るといくつかサンプルが書いてあります。
以下のテストを見てみます。
describe "plus" do
where(:a, :b, :answer) do
[
[1 , 2 , 3],
[5 , 8 , 13],
[0 , 0 , 0]
]
end
with_them do
it "should do additions" do
expect(a + b).to eq answer
end
end
where
ブロックにテストに渡すパラメータを定義しています。
with_them
ブロックにてパラメータを受け取ってテストを書きます。
先ほど書いたArticle
モデルのテストをパラメタライズドテストを使って書き換えると以下のようになりました。
app/models/article.rb
RSpec.describe Article, type: :model do
describe "validations" do
describe 'title' do
let(:article) { build(:article) }
where(:case_name, :it, :value, :is_valid, :error_message) do
[
['空でない場合', '有効であること', '記事タイトル', true, []],
['nilの場合', '無効であること', nil, false, ["を入力してください"]],
['空文字の場合', '無効であること', nil, false, ["を入力してください"]],
['文字数が254文字の場合', '無効であること', "a" * 254, true, []],
['文字数が255文字の場合', '無効であること', "a" * 255, true, []],
['文字数が256文字の場合', '無効であること', "a" * 256, false, ["は255文字以内で入力してください"]],
]
end
with_them do
it "#{params[:it]}" do
article.title = value
expect(article.valid?).to eq is_valid
expect(article.errors[:title]).to eq error_message
end
end
end
describe 'body' do
let(:article) { build(:article) }
where(:case_name, :it, :value, :is_valid, :error_message) do
[
['空でない場合', '有効であること', '記事タイトル', true, []],
['nilの場合', '無効であること', nil, false, ["を入力してください"]],
['空文字の場合', '無効であること', nil, false, ["を入力してください"]],
['文字数が9999文字の場合', '無効であること', "a" * 9999, true, []],
['文字数が10000文字の場合', '無効であること', "a" * 10000, true, []],
['文字数が10001文字の場合', '無効であること', "a" * 10001, false, ["は10000文字以内で入力してください"]],
]
end
with_them do
it "#{params[:it]}" do
article.body = value
expect(article.valid?).to eq is_valid
expect(article.errors[:body]).to eq error_message
end
end
end
describe 'slug' do
let!(:already_existing_article) { create(:article, slug: "already-existing-article") }
let(:article) { build(:article) }
where(:case_name, :it, :value, :is_valid, :error_message) do
[
['空でない場合', '有効であること', 'article-slug', true, []],
['nilの場合', '無効であること', nil, false, ["を入力してください"]],
['空文字の場合', '無効であること', nil, false, ["を入力してください"]],
['文字数が254文字の場合', '無効であること', "a" * 254, true, []],
['文字数が255文字の場合', '無効であること', "a" * 255, true, []],
['文字数が256文字の場合', '無効であること', "a" * 256, false, ["は255文字以内で入力してください"]],
['slugがすでに存在する場合', '無効であること', 'already-existing-article', false, ["はすでに存在しています"]],
]
end
with_them do
it "#{params[:it]}" do
article.slug = value
expect(article.valid?).to eq is_valid
expect(article.errors[:slug]).to eq error_message
end
end
end
describe '#published_at_must_be_in_the_future' do
let(:article) { build(:article) }
around do |example|
freeze_time { example.run }
end
where(:case_name, :it, :value, :is_valid, :error_message) do
now = Time.zone.now
[
['published_atがnilの場合', '有効であること', nil, true, []],
['published_atが未来の日付の場合', '有効であること', now + 1.day, true, []],
['published_atが現在日時の場合', '無効であること', now, false, ["は未来の日時を指定してください"]],
['published_atが過去の日付の場合', '無効であること', now - 1.day, false, ['は未来の日時を指定してください']],
]
end
with_them do
it "#{params[:it]}" do
article.published_at = value
expect(article.valid?).to eq is_valid
expect(article.errors[:published_at]).to eq error_message
end
end
end
end
end
テストパターンが網羅できているかひと目でわかり、見やすくなったのではないでしょうか?
まとめ
rspec-parameterizedを使うこと直感的に値が評価されるテストを書くことができるようになりました。
また、可読性が向上し、プルリクエストのレビューの際も認知負荷が減るのではないでしょうか?
Discussion