RSpec.describe Stack, type: :model do
let!(:stack) { Stack.new }
describe '#push' do
context '文字列をpushしたとき' do
it '返り値がpushした値であること' do
expect(stack.push('value')).to eq 'value'
end
end
context 'nilをpushした場合' do
it 'ArgumentErrorになること' do
expect { stack.push(nil) }.to raise_error(ArgumentError)
end
end
end
describe '#pop' do
context 'スタックが空の場合' do
it '返り値はnilであること' do
expect(stack.pop).to be_nil
end
end
context 'スタックに値があるとき' do
before do
stack.push 'value1'
stack.push 'value2'
end
it '最後の値を取得すること' do
expect(stack.pop).to eq 'value2'
end
end
end
end
FactoryBot.define do
factory :user do
sequence(:name) { |i| "username#{i}" }
after(:create) do |user, evaluator|
create_list(:post, 2, user: user)
end
end
end
RSpec.describe User, type: :model do
describe '#posts_ordered_by_popularity' do
let!(:user) { create(:user) }
let!(:post_popular) do
post = user.posts[0]
post.update(popularity: 5)
post
end
let!(:post_not_popular) do
post = user.posts[1]
post.update(popularity: 1)
post
end
it 'return posts ordered by popularity' do
expect(user.posts_ordered_by_popularity).to eq [post_popular, post_not_popular]
end
end
end
FactoryBot.define do
factory :user do
sequence(:name) { |i| "username#{i}" }
trait(:with_posts) do
after(:create) do |user, evaluator|
create_list(:post, 2, user: user)
end
end
end
end
class Post < ApplicationRecord
scope :last_month_published, -> { where(publish_at: (Time.zone.now - 31.days).all_month) }
end
require 'rails_helper'
RSpec.describe Post, type: :model do
describe '.last_month_published' do
let!(:april_1st) { create :post, publish_at: Time.zone.local(2017, 4, 1) }
let!(:april_30th) { create :post, publish_at: Time.zone.local(2017, 4, 30) }
before do
create :post, publish_at: Time.zone.local(2017, 5, 1)
create :post, publish_at: Time.zone.local(2017, 3, 31)
end
it 'return published posts in last month' do
Timecop.travel(2017, 5, 6) do
expect(Post.last_month_published).to contain_exactly(april_1st, april_30th)
end
end
end
end
このテストは常に成功するが、実装にはバグが含まれている。
テストを相対日時に変更してみる。
require 'rails_helper'
RSpec.describe Post, type: :model do
describe '.last_month_published' do
let!(:now) { Time.zone.now }
let!(:last_beginning_of_month) { create :post, publish_at: 1.month.ago(now).beginning_of_month }
let!(:last_end_of_month) { create :post, publish_at: 1.month.ago(now).end_of_month }
before do
create :post, publish_at: now
create :post, publish_at: 2.months.ago(now)
end
it 'return published posts in last month' do
expect(Post.last_month_published).to contain_exactly(last_beginning_of_month, last_end_of_month)
end
end
end
class User < ApplicationRecord
scope :active, -> { where(deleted: false).where.not(confirmed_at: nil) }
end
このテストをlet!のみを用いて書くと次のようになる。
require 'rails_helper'
RSpec.describe User, type: :model do
describe '.active' do
let!(:active) { create :user, deleted: false, confirmed_at: Time.zone.now }
let!(:deleted_but_confirmed) { create :user, deleted: true, confirmed_at: Time.zone.now }
let!(:deleted_and_not_confirmed) { create :user, deleted: true, confirmed_at: nil }
let!(:not_deleted_but_not_confirmed) { create :user, deleted: false, confirmed_at: nil }
it 'return active users' do
expect(User.active).to eq [active]
end
end
end
let!とbeforeを併用して書くと次のようになる。
require 'rails_helper'
RSpec.describe User, type: :model do
describe '.active' do
let!(:active) { create :user, deleted: false, confirmed_at: Time.zone.now }
before do
create :user, deleted: true, confirmed_at: Time.zone.now
create :user, deleted: true, confirmed_at: nil
create :user, deleted: false, confirmed_at: nil
end
it 'return active users' do
expect(User.active).to eq [active]
end
end
end
let!(:user) { create :user, enabled: enabled }
context 'when user is enabled' do
let(:enabled) { true }
it { ... }
end
context 'when user is disabled' do
let(:enabled) { false }
it { ... }
end
RSpec.describe Point, type: :model do
describe '#increase_by_day_of_the_week' do
let(:point) { create :point, point: 0 }
it_behaves_like 'point increasing by day of the week', 100 do
let(:wday) { 0 }
end
it_behaves_like 'point increasing by day of the week', 50 do
let(:wday) { 1 }
end
it_behaves_like 'point increasing by day of the week', 30 do
let(:wday) { 2 }
end
# ...
end
end
どんな前提条件で結果として何を期待しているのか、これだけを見て理解できるだろうか。
定義は次のようになる。
RSpec.shared_examples 'point increasing by day of the week' do |expected_point|
it "increase by #{expected_point}" do
expect(point.point).to eq 0
point.increase_by_day_of_the_week(wday)
expect(point.point).to eq expected_point
end
end
RSpec.shared_examples 'point increasing by day of the week' do |expected_point:|
it "increase by #{expected_point}" do
expect(point.point).to eq 0
point.increase_by_day_of_the_week(wday)
expect(point.point).to eq expected_point
end
end
RSpec.describe Point, type: :model do
describe '#increase_by_day_of_the_week' do
let(:point) { create :point, point: 0 }
context 'on sunday' do
let(:wday) { 0 }
it_behaves_like 'point increasing by day of the week', expected_point: 100
end
context 'on monday' do
let(:wday) { 1 }
it_behaves_like 'point increasing by day of the week', expected_point: 50
end
context 'on tuesday' do
let(:wday) { 2 }
it_behaves_like 'point increasing by day of the week', expected_point: 30
end
# ...
end
end
RSpec.describe Point, type: :model do
describe '#increase_by_day_of_the_week' do
let(:point) { create :point, point: 0 }
context 'on sunday' do
let(:wday) { 0 }
it "increase by 100" do
expect(point.point).to eq 0
point.increase_by_day_of_the_week(wday)
expect(point.point).to eq 100
end
end
context 'on monday' do
let(:wday) { 1 }
it "increase by 50" do
expect(point.point).to eq 0
point.increase_by_day_of_the_week(wday)
expect(point.point).to eq 50
end
end
context 'on tuesday' do
let(:wday) { 2 }
it "increase by 30" do
expect(point.point).to eq 0
point.increase_by_day_of_the_week(wday)
expect(point.point).to eq 30
end
end
# ...
end
end
describe 'sample specs' do
context 'a' do
# ...
end
context 'b' do
let!(:need_in_b_and_c) { ... }
# ...
end
context 'c' do
let!(:need_in_b_and_c) { ... }
# ...
end
end
この場合、b と c で同じ前提条件を利用しているので、一つ上のレベルに移動してDRYにしようと考える人もいるかもしれない。
describe 'sample specs' do
let!(:need_in_b_and_c) { ... }
context 'a' do
# ...
end
context 'b' do
# ...
end
context 'c' do
# ...
end
end
let!(:user) { create :user, enabled: enabled }
context 'when user is enabled' do
let(:enabled) { true }
it { ... }
end
context 'when user is disabled' do
let(:enabled) { false }
it { ... }
end
必要ないレコードを作らない
パフォーマンスの観点から、レコードを作らなくてすむ場合は作らないようにしたい。
describe 'posts#index' do
context 'when visit /posts' do
let!(:posts) { create_list :post, 100 }
before { visit posts_path }
it 'display all post titles' do
posts.each do |post|
expect(page).to have_content post.title
end
end
end
end
describe 'posts#index' do
context 'when visit /posts' do
let!(:post) { create :post }
before { visit posts_path }
it 'display post title' do
expect(page).to have_content post.title
end
end
end
モデルのユニットテストでも、作らなくてよいレコードを作っているケースはよくある。
RSpec.describe User, type: :model do
describe '#fullname' do
let!(:user) { create :user, first_name: 'Shinichi', last_name: 'Maeshima' }
it 'return full name' do
expect(user.fullname).to eq 'Shinichi Maeshima'
end
end
end
RSpec.describe User, type: :model do
describe '#fullname' do
let!(:user) { build :user, first_name: 'Shinichi', last_name: 'Maeshima' }
it 'return full name' do
expect(user.fullname).to eq 'Shinichi Maeshima'
end
end
end
RSpec.describe Post, type: :model do
let!(:post) { create :post }
describe '#published?' do
subject { post.published? }
context 'when the post has already published' do
it { is_expected.to eq true }
end
context 'when the post has not published' do
before { post.update(publish_at: nil) }
it { is_expected.to eq false }
end
context 'when the post is closed' do
before { post.update(status: :close) }
it { is_expected.to eq false }
end
context 'when the title includes "[WIP]"' do
before { post.update(title: '[WIP]hello world') }
it { is_expected.to eq false }
end
end
end
RSpec.describe Post, type: :model do
let!(:post) { create :post, title: title, status: status, publish_at: publish_at }
let(:title) { 'hello world' }
let(:status) { :open }
let(:publish_at) { Time.zone.now }
describe '#published?' do
subject { post.published? }
context 'when the post has already published' do
it { is_expected.to eq true }
end
context 'when the post has not published' do
let(:publish_at) { nil }
it { is_expected.to eq false }
end
context 'when the post is closed' do
let(:status) { :close }
it { is_expected.to eq false }
end
context 'when the title includes "[WIP]"' do
let(:title) { '[WIP]hello world'}
it { is_expected.to eq false }
end
end
end
describe 'ApiClient#save_record_from_api' do
let!(:client) { ApiClient.new }
subject(:execute_api_with_params) { client.save_record_from_api(params) }
context 'when pass { limit: 10 }' do
let(:params) { { limit: 10} }
it 'return ApiResponse object' do
is_expected.to be_an_instance_of ApiResponse
end
it 'save 10 items' do
expect { execute_api_with_params }.to change { Item.count }.by(10)
end
end
end
class Statement
def issue(body)
client = TwitterClient.new
client.issue(body)
end
end
RSpec.describe Statement do
describe '#issue' do
let!(:statement) { Statement.new }
it 'call TwitterClient#issue' do
expect_any_instance_of(TwitterClient).to receive(:issue).with('hello')
statement.issue('hello')
end
end
end
class Statement
def initialize(client: TwitterClient.new)
@client = client
end
def issue(body)
client.issue(body)
end
private
def client
@client
end
end
RSpec.describe Statement do
describe '#issue' do
let!(:client) { double('client') }
let!(:statement) { Statement.new(client: client) }
it 'call TwitterClient#issue' do
expect(client).to receive(:issue).with('hello')
statement.issue('hello')
end
end
end