I have just finished reading Testing Rails guide by thoughtbot. The author mentions that using let! or let is an antipattern because in your test you dont get to see the variables that are needed for the setup.
I have this one common case that I'm curious how to deal with however if lets would be removed. I'm creating tests for creating a cart. When a cart is created, many things are tested. The current test is as follows (I removed the implementation of some methods and just kept the structure for brevity):
RSpec.describe Mutations::Carts::CreateCart, type: :request do
context 'Sending a request with an unauthorized user' do
it 'Creates a new cart and set the table for it' do
# Test setup
store = create :store
branch = store.branches.first
table = create :table, branch: branch
create_dependencies_for_create_cart(store)
cart_count_before_request = Cart.count
# Exercise
send_create_cart(store.id, table.id)
# Verify: (Some expects)
end
it 'Returns the branch id and and the table id' do
# Test setup
store = create :store
branch = store.branches.first
table = create :table, branch: branch
create_dependencies_for_create_cart(store)
# Exercise
send_create_cart(store.id, table.id)
# Verify: Some expects
end
end
private
def send_create_cart(store_id, table_id)
# Prepare and send the request
end
def create_dependencies_for_create_cart(store)
# Creating some objects that need to be created before the cart is created
end
end
My problem here is that the test setup is common and needs to only run once. Also the request can be sent once.
One tiny improvement over this would be to add a before(:each). This would on the one hand DRY the code a bit, but on the other hand, I'm still running the setup + exercise twice potentially slowing my tests down, and I'm making the test less readable. before(:all) would at least ensure that the setup and exercise are only run once, still this is deperacted and recommended against by the Ruby community.
What's the clean and effecient way (both readable and requires no repeated setups) to test such cases?
All create should be set via let. Keep in mind that all your specs must be independent all the time. No matter the order they have been run.
If you've a test that checks if a new cart has be created. That's one test. Another one might be to check the response.
Even if best practices say it's better to have a single expect per spec, sometime it makes sense to have several. It's all about context and logical for other dev to debug your code later on.
Here is my proposition:
context 'Sending a request with an unauthorized user' do
subject { send_create_cart(store.id, table.id) }
let(:store) { create(:store) }
let(:branch) { create(:branch, store: store)} # check according to your association
let!(:table) { create(:table, branch: branch) }
# Create dependencies for cart here, on `let` too.
# use `!` to force creation
# Here only `table` has `!` since other variable are called to create this one.
it 'Creates a new cart and set the table for it' do
expect{ subject }.to change { Cart.count }.by(1)
end
it 'Returns the branch id and and the table id' do
expect(subject).to eq([branch.id, table.id])
end
private
def send_create_cart(store_id, table_id)
# Prepare and send the request
end
end
A good approach would be using a describe with a context with a different describes inside it each having its own before do.
describe 'Sending a request' do
context ' with an unauthorized user' do
store = create :restaurant
branch = store.branches.first
table = create :table, branch: branch
describe 'creating cart and table' do
before do
create_dependencies(store)
cart_count_before_request = Cart.count
# Exercise
send_create_cart()
end
it 'should not create a cart' do
# Verify: (Some expects)
end
it 'returns the ids'do
# Verify: Some expects
end
end
end
end
Related
I don't understand why my record in 2 spec is not rollback:
I have 2 tables, prefecture has_many hospitals
require "rails_helper"
RSpec.describe Hospital, type: :model, focus: true do
context "created along with new hospital created" do
it "should create new hospital" do
#prefecture = FactoryGirl.create(:prefecture)
hospital = FactoryGirl.build(:hospital, id: 10, prefecture: #prefecture)
expect { hospital.save }.to change { Hospital.count }.by 1
end
it "should save" do
#prefecture = FactoryGirl.create(:prefecture)
hospital = FactoryGirl.build(:hospital, id: 10, prefecture: #prefecture)
hospital.save
end
end
end
If I run it will show error "id=10 is existed in db"
Can anyone explain where I am mistake?
Thank you.
It is not the responsibility of FactoryGirl. You have 2 options:
Use transactions in tests: https://relishapp.com/rspec/rspec-rails/docs/transactions
Use a gem which cleans DB after after test such as: https://github.com/DatabaseCleaner/database_cleaner
Usually your models test will use transctions and database cleaner is used for integration/acceptance tests which cannot operate within transaction because a separate process (browser) needs to read the data created in tests.
By default, the rspec will not reset your database for each test. You need to configure it. Is because that your second test is falling, already exists a Hospital with id = 10.
When you remove the parameter id for the second test and runs with success, FactoryGirl will generate automatically a new id.
The method build from FactoryGirl will not fill the attribute id, just the rest of the attributes. Is because that when you call save after build method, the FactoryGirll generate a new id and your tests pass successfully.
after :build do |offer|
offer.save
end
We currently face a problem when a request spec fails (typo or other exception) it affects other request specs which actually should be unaffected. The problem is we use many gems so I tried to create a minimal reproduction app (without success, https://github.com/tak1n/reproduction)
The problem in detail:
We have a request spec where we need to login a user. For user authentication we use devise and therefore we use Warden::Test::Helpers to log in users in request specs. Devise automatically updates some user related attributes when a user logs in (last_sign_in_at, last_sign_in_ip etc..).
The problem here is that when it tries to save the user for doing this changes the end result is a INSERT INTO user instead of a UPDATE user and therefore it crashes.
Here is an example spec from our app:
require 'rails_helper'
RSpec.describe 'Suggestions API' do
let(:user) { FactoryGirl.create(:user, :professional) }
before do
login(user) # same as in https://github.com/tak1n/reproduction/blob/master/spec/support/request_macros.rb#L6
end
describe '/suggestions.json', :vcr do
context 'with non saved filter' do
# some setup stuff here (setup proper objects in db)
# also params are defined here through let(:params) { ... }
it 'returns proper suggestions' do
Suggestion.refresh!
get '/suggestions.json', paramst
expect(json.count).to eq(2)
expect(json.first['id']).to eq(sug2.id)
expect(json.second['id']).to eq(sug1.id)
end
end
context 'with saved filter' do
# some setup stuff here (setup proper objects in db)
# also params are defined here through let(:params) { ... }
it 'returns proper suggestions' do
Suggestion.refresh!
get '/suggestions.json', params
expect(json.count).to eq(2)
expect(json.first['id']).to eq(sug2.id)
expect(json.second['id']).to eq(sug1.id)
end
end
end
end
The result of this should be that the first spec fails because of the typo paramst, which it does but it also affects the second spec:
Here is the spec run: https://gist.github.com/tak1n/102c1aa121b66e0ab56602b76f911ec0
I tried to dig deeper and saw that the logic whether save creates or updates a record is depending on https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L85
Next I tried to output what #new_record was in that case.
def new_record?
sync_with_transaction_state
puts "New record: #{#new_record}" if self.class == User
#new_record
end
I got following: https://gist.github.com/tak1n/330560a3a108abc8fce4d105a48ac444
When the specs run in different order (non failing first then spec with typo or exception in it) I got this: https://gist.github.com/tak1n/6eb24693226d8e6a713c0865ea1bebd5
The difference here is for the "working" spec run New Record: <boolean> is different. When the spec with the exception runs before it New record suddenly becomes true and therefore it generates SQL to create a new record.
Next I assumed some gems are causing this problem, most likely I thought it would be database_cleaner but in the reproduction I do more or less the same and there it works.
The problem is I'm really stuck at debugging and not sure where to proceed. Also I'm not sure which gem or our code itself is causing this.
Feel free to ask any questions if you need further details, thanks in advance.
Seems like the issue is caused by devise-async (https://github.com/mhfs/devise-async).
See https://github.com/rails/rails/issues/26038 and https://github.com/mhfs/devise-async/issues/96 for more information.
Let's say we have class:
class Post
def save
# implementation
end
def self.find(id)
#implementation
end
end
I struggle with testing #save and .find, I've:
describe '#save' do
it 'saves the post' do
subject.save
created = Post.find(subject.id)
expect(created).to eq(subject)
end
end
describe '.find' do
it 'finds the post' do
subject.save
created = Post.find(subject.id)
expect(created).to eq(subject)
end
end
In case of #save method I'd like to check side effect, in case of .find I'd like to test returned value. How to cope with this case without duplicating specs ?
In this case, to isolate the save and find actions, you need to mock the repository.
Whether you are writing to a DB, a file-system, cache, or whatever - you can mock it to either expect the saving feature, or set it up (before the beginning of the test) to make sure find works.
For most repository implementations there are gems to mock them (Factory Girl for relational databases, FakeFS for file-system), but you can roll your own if you have some exotic repository no one has heard of.
This way you test save without using find, or vice versa.
My Rails models: task has_many positions.
Scenario: When I create a new position, it should create itself a task. I'd like to test that, and I'm doing it like this:
context "creating a new position" do
let(:position) { create :position, name: 'Read some books', :task => nil }
it "should create a simple task" do
Task.find_by_name('Read some books').should be_nil # First should
position # Execute let() block (FactoryGirl is lazy evaluating)
Task.find_by_name('Read some books').should_not be_nil # Second (more relevant) should
end
end
So how should I improve my test? The first "should" simply makes sure that there isn't already a Task, so we can be sure that creating the Position creates the Task. But this violates the "only one should per it block" principle. So what about this?
context "creating a new position" do
let(:position) do
position = create :position, name: 'Read some books', :task => nil
Task.delete_all
position
end
it "should create a simple task" do
position # Execute let() block (FactoryGirl is lazy evaluating)
Task.find_by_name('Read some books').should_not be_nil
end
end
Or should I simply count on the fact that there shouldn't be such a task anyways (because a clean test db wouldn't have one)? Thanks for your opinions.
Update (Solution)
After some research I found the change matcher of RSpec:
let(:position) { create :position, name: 'Read some books', :task => nil }
it "should create a simple task" do
# Thanks to FactoryGirl's lazy evaluation of let(), the position doesn't yet exist in the first place, and then after calling position in the expect{} block, it is created.
expect { position }.to change{ Task.count(conditions: { name: 'Read some books' }) }.by(1)
end
What to Test
I will not address in detail whether the tests themselves are useful to any degree. To me, they seem to be exercising basic database functions rather than application logic, which is of marginal utility, but only you can really decide what's important to test.
Be Specific
In the example you give, there's no real reason to use a let block, which memoizes the variable. If only one test needs the record, instantiate it just in that specific test. For example:
context 'creating a new position' do
it 'should be nil when the position record is missing' do
Task.find_by_name('Read some books').should be_nil
end
it 'should successfully create a position' do
create :position, name: 'Read some books', :task => nil
Task.find_by_name('Read some books').should_not be_nil
end
end
Alternatively, if you're trying to test how your application behaves when a record is missing, then go ahead and memoize a variable or create a record in a before block, but explicitly delete the record in that one specific test.
Multiple Contexts
Finally, if you're finding that you have too much state to set up in individual tests, that's usually a clue that you should consider splitting your tests into different contexts. For example, you might want to separate tests into one context that checks behavior when a record doesn't exist, and a separate context for when records do exist.
Like all things testing, it's an art more than a science. Your mileage may vary.
RSpec 2.11 allows you to pass a block to change, and it expects the return value of the block to be the thing that changes. I would expect this to work for you:
expect { position }.to change { Task.where(:name => 'Read some books').count }.from(0).to(1)
I'm writing integration tests using Rspec and Capybara. I've noticed that quite often I have to execute the same bits of code when it comes to testing the creation of activerecord options.
For instance:
it "should create a new instance" do
# I create an instance here
end
it "should do something based on a new instance" do
# I create an instance here
# I click into the record and add a sub record, or something else
end
The problem seems to be that ActiveRecord objects aren't persisted across tests, however Capybara by default maintains the same session in a spec (weirdness).
I could mock these records, but since this is an integration test and some of these records are pretty complicated (they have image attachments and whatnot) it's much simpler to use Capybara and fill out the user-facing forms.
I've tried defining a function that creates a new record, but that doesn't feel right for some reason. What's the best practice for this?
There are a couple different ways to go here. First of all, in both cases, you can group your example blocks under either a describe or context block, like this:
describe "your instance" do
it "..." do
# do stuff here
end
it "..." do
# do other stuff here
end
end
Then, within the describe or context block, you can set up state that can be used in all the examples, like this:
describe "your instance" do
# run before each example block under the describe block
before(:each) do
# I create an instance here
end
it "creates a new instance" do
# do stuff here
end
it "do something based on a new instance" do
# do other stuff here
end
end
As an alternative to the before(:each) block, you can also use let helper, which I find a little more readable. You can see more about it here.
The very best practice for your requirements is to use Factory Girl for creating records from a blueprint which define common attributes and database_cleaner to clean database across different tests/specs.
And never keep state (such as created records) across different specs, it will lead to dependent specs. You could spot this kind of dependencies using the --order rand option of rspec. If your specs fails randomly you have this kind of issue.
Given the title (...reusing code in Rspec) I suggest the reading of RSpec custom matchers in the "Ruby on Rails Tutorial".
Michael Hartl suggests two solutions to duplication in specs:
Define helper methods for common operations (e.g. log in a user)
Define custom matchers
Use these stuff help decoupling the tests from the implementation.
In addition to these I suggest (as Fabio said) to use FactoryGirl.
You could check my sample rails project. You could find there: https://github.com/lucassus/locomotive
how to use factory_girl
some examples of custom matchers and macros (in spec/support)
how to use shared_examples
and finally how to use very nice shoulda-macros
I would use a combination of factory_girl and Rspec's let method:
describe User do
let(:user) { create :user } # 'create' is a factory_girl method, that will save a new user in the test database
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
end
# spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
username { Faker::Internet.user_name }
end
end
This allows you to do great stuff like this:
describe User do
let(:user) { create :user, attributes }
let(:attributes) { Hash.new }
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
context "when user is admin" do
let(:attributes) { { admin: true } }
it "should be able to walk" do
user.walk.should be_true
end
end
end