Reputation: 4804
I how such a test block in my Rails app with RSpec:
describe "POST create" do
describe "if we have a logged in user and he can be an owner" do
describe "and if params are valid" do
let!(:service_attributes_with_categories_1_and_2) {
FactoryBot.attributes_for :service_with_categories_1_and_2
}
let!(:category_1) { FactoryBot.create :category, {id: 1} }
let!(:category_2) { FactoryBot.create :category, {id: 2} }
it "creates a new service" do
# ...
end
it "creates associations with categories" do
# ...
end
end
describe "and if categories are not valid" do
# ...
end
describe "and if some common service params are not valid" do
# ...
end
end
describe "if no user is logged in, but params are valid" do
let!(:service_attributes_with_categories_1_and_2) {
FactoryBot.attributes_for :service_with_categories_1_and_2
}
let!(:category_1) { FactoryBot.create :category, {id: 1} }
let!(:category_2) { FactoryBot.create :category, {id: 2} }
it "doesn't create a new service" do
# ...
end
it "doesn't create associations with categories" do
# ...
end
end
describe "if logged user cannot be an owner, but params are valid" do
let!(:service_attributes_with_categories_1_and_2) {
FactoryBot.attributes_for :service_with_categories_1_and_2
}
let!(:category_1) { FactoryBot.create :category, {id: 1} }
let!(:category_2) { FactoryBot.create :category, {id: 2} }
it "doesn't create a new service" do
# ...
end
it "doesn't create associations with categories" do
# ...
end
end
end
As we can see, I have many redundant let!
method calls, but I don't know how I could make it DRY. I cannot just define plain method, because in that case variables will be available in this method's scope only. I cannot also let my categories to be created in general scope, because in two cases they shouldn't be created because of test nature. So, how should I technically do that?
Upvotes: 1
Views: 217
Reputation: 415
You can make your spec DRY as below :
1) Use let
instead of defining method.
2) Arrange your context
blocks in such a way that further cases will be accommodated easily.
describe 'POST create' do
let!(:service_attributes_with_categories_1_and_2) {
FactoryBot.attributes_for :service_with_categories_1_and_2
}
let!(:category_1) { FactoryBot.build :category, {id: 1} }
let!(:category_2) { FactoryBot.build :category, {id: 2} }
let(:save_categories_1_and_2) { category_1.save && category_2.save }
context 'when user is logged in' do
context 'when user is an owner' do
context 'when params are valid' do
before do
save_categories_1_and_2
end
it 'creates a new service' do
end
it 'creates associations with categories' do
end
end
context 'when categories are not valid' do
end
context 'when some common service params are not valid' do
end
end
context 'when user is not an owner' do
context 'when params are valid' do
before do
save_categories_1_and_2
end
it "doesn't create a new service" do
end
it "doesn't create associations with categories" do
end
end
end
end
context 'when no user is logged in' do
context 'when params are valid' do
before do
save_categories_1_and_2
end
it "doesn't create a new service" do
end
it "doesn't create associations with categories" do
end
end
end
end
Upvotes: 1
Reputation: 4804
Finally I decided to split the FactoryBot.create
function to two steps: FactoryBot.build
and .save
function run on obtained object. It allowed me to move my let!
calls to the main scope, and to define method, which saves my built objects exactly in that cases where I need it. My DRY code looks like that now:
describe "POST create" do
let!(:service_attributes_with_categories_1_and_2) {
FactoryBot.attributes_for :service_with_categories_1_and_2
}
let!(:category_1) { FactoryBot.build :category, {id: 1} }
let!(:category_2) { FactoryBot.build :category, {id: 2} }
def save_categories_1_and_2
category_1.save
category_2.save
end
describe "if we have a logged in user and he can be an owner" do
describe "and if params are valid" do
before(:each) do
save_categories_1_and_2
end
it "creates a new service" do
# ...
end
it "creates associations with categories" do
# ...
end
end
describe "and if categories are not valid" do
# ...
end
describe "and if some common service params are not valid" do
# ...
end
end
describe "if no user is logged in, but params are valid" do
before(:each) do
save_categories_1_and_2
end
it "doesn't create a new service" do
# ...
end
it "doesn't create associations with categories" do
# ...
end
end
describe "if logged user cannot be an owner, but params are valid" do
before(:each) do
save_categories_1_and_2
end
it "doesn't create a new service" do
# ...
end
it "doesn't create associations with categories" do
# ...
end
end
end
Many thanks to @midlins and @Taryn East for pointing me to the right track. @Taryn East - your suggested adjustment would also work in the case described there, but in my app I have also more advanced cases, for which that's not enough. I think that the solution presented here is more universal.
Upvotes: 0