DRY up context and subject repetition while testing many states - ruby-on-rails

I've finally taken the plunge and I'm learning RSpec after years of barely doing any automated testing.
I'm testing a model with 18 possible state combinations. Currently, my spec looks something like this simplified example (note that create comes from factory_girl):
describe MyModel do
context 'in unapproved state' do
context 'with guest account' do
subject { build_stubbed(:my_model, :unapproved, :guest) }
describe '#method_a' do
it { ... }
end
describe '#method_b' do
it { ... }
end
end
context 'with full account' do
subject { build_stubbed(:my_model, :unapproved, :full) }
describe '#method_a' do
it { ... }
end
describe '#method_b' do
it { ... }
end
end
end
context 'in approved state' do
context 'with guest account' do
subject { build_stubbed(:my_model, :approved, :guest) }
describe '#method_a' do
it { ... }
end
describe '#method_b' do
it { ... }
end
end
context 'with full account' do
subject { build_stubbed(:my_model, :approved, :full) }
describe '#method_a' do
it { ... }
end
describe '#method_b' do
it { ... }
end
end
end
end
This works great and I'm (mostly) happy with it. However, I've been doing a lot of reading on RSpec best practices and it appeears I should probably put all my describes at the outermost level and nest contexts inside them (opposite of what I have above).
Putting describes at the outermost level makes perfect sense to me as it groups together the components of your class (the things you're actually testing). My example above would become:
describe MyModel do
describe '#method_a' do
context 'in unapproved state' do
context 'with guest account' do
subject { build_stubbed(:my_model, :unapproved, :guest) }
it { ... }
end
context 'with full account' do
subject { build_stubbed(:my_model, :unapproved, :full) }
it { ... }
end
end
context 'in approved state' do
context 'with guest account' do
subject { build_stubbed(:my_model, :approved, :guest) }
it { ... }
end
context 'with full account' do
subject { build_stubbed(:my_model, :approved, :full) }
it { ... }
end
end
end
describe '#method_a' do
context 'in unapproved state' do
context 'with guest account' do
subject { build_stubbed(:my_model, :unapproved, :guest) }
it { ... }
end
context 'with full account' do
subject { build_stubbed(:my_model, :unapproved, :full) }
it { ... }
end
end
context 'in approved state' do
context 'with guest account' do
subject { build_stubbed(:my_model, :approved, :guest) }
it { ... }
end
context 'with full account' do
subject { build_stubbed(:my_model, :approved, :full) }
it { ... }
end
end
end
end
This is cool and I think it makes my spec easier to read and to navigate. However, there's now a lot of repetition with the contexts and subjects.
What's a good way to reduce the repetition in this scenario?

Related

How to test this method? (RSpec)

def is_doctor
user = User.find_by(role: 'doctor')
"Name: #{user.name} | Email: #{user.email}| isActive: #{user.is_active}"
end
Something like this, but I don't know how to implement it correctly ↓
context 'test' do
#it { expect(user.is_doctor).to eq("Taras") }
end
I assume doctor? is an instance method on a User model + you're using Faker which might help you a lot.
# models/user.rb
class User < ApplicationRecord
def doctor?
return 'Not a doc' unless role == 'doctor'
"Name: #{name} | Email: #{email}| isActive: #{is_active}"
end
end
# specs/models/user_spec.rb
describe User, type: :model do
context 'with instance method' do
describe '#doctor?' do
subject { user. doctor? }
context 'with a doctor' do
let(:user) { create(:user, role: 'doctor') }
it 'includes name' do
expect(subject).to include("Name: #{user.name}")
end
it 'includes email' do
expect(subject).to include("Email: #{email}")
end
it 'includes is_active' do
expect(subject).to include("isActive: #{is_active}")
end
end
context 'without doctor' do
let(:user) { create(:user, role: 'foo') }
it 'has static response' do
expect(subject).to eq('Not a doc')
end
end
end
end
end

Rspec DRY: apply example to all contexts

is it possible to shorten this Rspec?
I'd like to extract the line it { expect { author.destroy }.to_not raise_error } not to repeat it in every context. Shared examples are some way, but finally, it generates more code than below redundant version.
require 'rails_helper'
RSpec.describe Author, type: :model do
describe 'destroying' do
context 'when no books assigned' do
subject!(:author) { FactoryBot.create :author_with_no_books }
it { expect { author.destroy }.to_not raise_error }
# other examples
end
context 'when there are some books' do
subject!(:author) { FactoryBot.create :author_with_books }
it { expect { author.destroy }.to_not raise_error }
# other examples
end
context 'when there are some posts' do
subject!(:author) { FactoryBot.create :author_with_posts }
it { expect { author.destroy }.to_not raise_error }
# other examples
end
end
end
Use shared_examples with a parameter instead of abusing subject:
RSpec.describe Author, type: :model do
include FactoryBot::Syntax::Methods # you can move this to rails_helper.rb
RSpec.shared_examples "can be destroyed" do |thing|
it "can be destroyed" do
expect { thing.destroy }.to_not raise_error
end
end
describe 'destroying' do
context 'without books' do
include_examples "can be destroyed", create(:author_with_no_books)
end
context 'with books' do
include_examples "can be destroyed", create(:author_with_books)
end
context 'with posts' do
include_examples "can be destroyed", create(:author_with_posts)
end
end
end

How to assert that a method call was not made, without any_instance?

I have a class, that in one situation should call :my_method, but in another situation must not call method :my_method. I would like to test both cases. Also, I would like the test to document the cases when :my_method should not be called.
Using any_instance is generally discouraged, so I would be happy to learn a nice way to replace it.
This code snippet is a reduced example on what I kind of test I would like to write.
class TestSubject
def call
call_me
end
def call_me; end
def never_mind; end
end
require 'rspec'
spec = RSpec.describe 'TestSubject' do
describe '#call' do
it 'calls #call_me' do
expect_any_instance_of(TestSubject).to receive(:call_me)
TestSubject.new.call
end
it 'does not call #never_mind' do
expect_any_instance_of(TestSubject).not_to receive(:never_mind)
TestSubject.new.call
end
end
end
spec.run # => true
It works, but uses expect_any_instance_of method, which is not recommended.
How to replace it?
I'll do somehting like that
describe TestSubject do
describe '#call' do
it 'does not call #something' do
subject = TestSubject.new
allow(subject).to receive(:something)
subject.call
expect(subject).not_to have_received(:something)
end
end
end
Hope this helped !
This is how I normally unit-test. I updated the code to support other possible questions you (or other readers) may have in the future.
class TestSubject
def call
some_call_me_value = call_me
call_you(some_call_me_value)
end
def call_me; end
def call_you(x); end
def never_mind; end
class << self
def some_class_method_a; end
def some_class_method_b(x, y); end
end
end
require 'rspec'
spec = RSpec.describe TestSubject do
context 'instance methods' do
let(:test_subject) { TestSubject.new }
describe '#call' do
let(:args) { nil }
let(:mocked_call_me_return_value) { 'somecallmevalue' }
subject { test_subject.call(*args) }
before do
allow(test_subject).to receive(:call_me) do
mocked_call_me_return_value
end
end
it 'calls #call_me' do
expect(test_subject).to receive(:call_me).once
subject
end
it 'calls #call_you with call_me value as the argument' do
expect(test_subject).to receive(:call_you).once.with(mocked_call_me_return_value)
subject
end
it 'does not call #never_mind' do
expect(test_subject).to_not receive(:never_mind)
subject
end
it 'calls in order' do
expect(test_subject).to receive(:call_me).once.ordered
expect(test_subject).to receive(:call_you).once.ordered
subject
end
end
describe '#call_me' do
let(:args) { nil }
subject { test_subject.call_me(*args) }
# it ...
end
describe '#call_you' do
let(:args) { nil }
subject { test_subject.call_you(*args) }
shared_examples_for 'shared #call_you behaviours' do
it 'calls your phone number'
it 'creates a Conversation record'
end
# just an example of argument-dependent behaviour spec
context 'when argument is true' do
let(:args) { [true] }
it 'does something magical'
it_behaves_like 'shared #call_you behaviours'
end
# just an example of argument-dependent behaviour spec
context 'when argument is false' do
let(:args) { [false] }
it 'does something explosive'
it_behaves_like 'shared #call_you behaviours'
end
end
end
context 'class methods' do
let(:args) { nil }
describe '#some_class_method_a' do
let(:args) { nil }
subject { TestSubject.some_class_method_a(*args) }
# it ...
end
describe '#some_class_method_b' do
let(:args) { [1, 2] }
subject { TestSubject.some_class_method_b(*args) }
# it ...
end
end
end
spec.run # => true
Do not test if some method was called or wasn't.
This will tight your tests to the implementation details and will force you to change tests every time you refactor(change implementation details without changing the behaviour) your class under test.
Instead test against return value or changed application state.
It is difficult come up with the example, you didn't provide enough context about the class under the test.
class CreateEntity
def initialize(name)
#name = name
end
def call
if company_name?(#name)
create_company
else
create_person
end
end
def create_person
Person.create!(:name => #name)
end
def create_company
Company.create!(:name => #name)
end
end
# tests
RSpec.describe CreateEntity do
let(:create) { CreateEntity.new(name).call }
describe '#call' do
context 'when person name is given' do
let(:name) { 'Firstname Lastname' }
it 'creates a person' do
expect { create }.to change { Person.count }.by(1)
end
it 'do not create a company' do
expect { create }.not_to change { Company.count }
end
end
context 'when company name is given' do
let(:name) { 'Name & Sons Ltd' }
it 'creates a company' do
expect { create }.to change { Company.count }.by(1)
end
it 'do not create a person' do
expect { create }.not_to change { Person.count }
end
end
end
end
With tests above I would be able to change how CreateEntity.call method implemented without changing tests as far as behaviour remain same.

rspec + shoulda: setting up data

I have the following test. There are three it blocks. The first one doesn't use shoulda unlike the other two.
If I don't use the before block with post :create, product: attrs then the first test fails as expected. But If I put the before block there then the first test fails, but the other two pass. I have a uniqueness validation on product name, but that shouldn't be the problem as I'm using sequence with factory.
What should I do? How should I generally setup the data for testing when there are rspec and shoulda matchers present at the same time?
describe "when user logged in" do
before(:each) do
login_user #logged in user is available by calling #user
end
context "POST create" do
context "with valid attributes" do
let!(:profile) { create(:profile, user: #user) }
let!(:industry) { create(:industry) }
let!(:attrs) { attributes_for(:product, user_id: #user.id, industry_ids: [ industry.id ]).merge(
product_features_attributes: [attributes_for(:product_feature)],
product_competitions_attributes: [attributes_for(:product_competition)],
product_usecases_attributes: [attributes_for(:product_usecase)]
) }
it "saves the new product in the db" do
expect{ post :create, product: attrs }.to change{ Product.count }.by(1)
end
#If I don't use this the 2 tests below fail. If I use it, then the test above fails.
# before do
# post :create, product: attrs
# end
it { is_expected.to redirect_to product_path(Product.last) }
it { is_expected.to set_flash.to('Product got created!') }
end
end
end
factories
factory :product, class: Product do
#name { Faker::Commerce.product_name }
sequence(:name) { |n| "ABC_#{n}" }
company { Faker::Company.name }
website { 'https://example.com' }
oneliner { Faker::Lorem.sentence }
description { Faker::Lorem.paragraph }
user
end
You can't have it both ways. If you execute the method you are testing in the before, then you can't execute it again to see if it changes the Product count. If you don't execute it in your before, then you must execute it in your example and therefore can't use the is_expected one liner format.
There are a variety of alternatives. Here is one that incorporates the execution of the method into all the examples.
describe "when user logged in" do
before(:each) do
login_user #logged in user is available by calling #user
end
describe "POST create" do
subject(:create) { post :create, product: attrs }
context "with valid attributes" do
let!(:profile) { create(:profile, user: #user) }
let!(:industry) { create(:industry) }
let!(:attrs) { attributes_for(:product, user_id: #user.id, industry_ids: [ industry.id ]).merge(
product_features_attributes: [attributes_for(:product_feature)],
product_competitions_attributes: [attributes_for(:product_competition)],
product_usecases_attributes: [attributes_for(:product_usecase)]
) }
it "saves the new product in the db" do
expect{ create }.to change{ Product.count }.by(1)
end
it("redirects") { expect(create).to redirect_to product_path(Product.last) }
it("flashes") { expect(create).to set_flash.to('Product got created!') }
end
end
end

RSpec, how to test initialize inside a module

I have this module:
module TicketsPresenters
class ShowPresenter
def initialize(ticket_or_id = nil)
unless ticket_or_id.nil?
if ticket_or_id.is_a?(Ticket)
#ticket = ticket_or_id
else
#ticket = Ticket.find(ticket_or_id)
end
end
end
end
I'd like to test if the initialize() method sets up the object properly, when I pass an integer or directly the object instance.
There's probably a dozen ways to answer this, but I'll give you the RSpec format that I prefer.
The following assumes you have a reader method for ticket (ie attr_reader :ticket) in your ShowPresenter class. It also assumes you are creating your Ticket object with valid parameters so it gets saved.
describe TicketsPresenters::ShowPresenter do
context '#initialize' do
let!(:ticket) { Ticket.create!(...) }
context 'with an id' do
subject { TicketsPresenters::ShowPresenter.new(ticket.id) }
its(:ticket) { should == ticket }
end
context 'with a Ticket object' do
subject { TicketsPresenters::ShowPresenter.new(ticket) }
its(:ticket) { should == ticket }
end
context 'with nothing' do
subject { TicketsPresenters::ShowPresenter.new }
its(:ticket) { should be_nil }
end
end
end
Note: I'm a fan of FactoryGirl so I would personally prefer to use Factory.create(:ticket) over Ticket.create!(...) since it allows you define a valid Ticket object in one place and you don't need to update it in all your tests if that definition changes.
Another testing position that people take is to not use database persistance at all. This is probably not a concept I would suggest to people new to Ruby or RSpec since it is a little harder to explain and would require more OOP knowledge. The upside is that it removes the database dependency and tests are faster and more isolated.
describe TicketsPresenters::ShowPresenter do
context '#initialize' do
let(:ticket) { mock(:ticket, id: 1) }
before do
ticket.stub(:is_a?).with(Ticket) { true }
Ticket.stub(:find).with(ticket.id) { ticket }
end
context 'with an id' do
subject { TicketsPresenters::ShowPresenter.new(ticket.id) }
its(:ticket) { should == ticket }
end
context 'with a Ticket object' do
subject { TicketsPresenters::ShowPresenter.new(ticket) }
its(:ticket) { should == ticket }
end
context 'with nothing' do
subject { TicketsPresenters::ShowPresenter.new }
its(:ticket) { should be_nil }
end
end
end

Resources