I have a model Lead and a callback: after_commit :create, :send_to_SPL
I am using Rails-4.1.0, ruby-2.1.1, RSpec.
1) This spec is not passing:
context 'callbacks' do
it 'shall call \'send_to_SPL\' after create' do
expect(lead).to receive(:send_to_SPL)
lead = Lead.create(init_hash)
p lead.new_record? # => false
end
end
2) This spec is not passing too:
context 'callbacks' do
it 'shall call \'send_to_SPL\' after create' do
expect(ActiveSupport::Callbacks::Callback).to receive(:build)
lead = Lead.create(init_hash)
end
end
3) This one is passing, but I think it is not testing after_commit callback:
context 'callbacks' do
it 'shall call \'send_to_SPL\' after create' do
expect(lead).to receive(:send_to_SPL)
lead.send(:send_to_SPL)
end
end
What is the best way to test after_commit callbacks in Rails?
I thought Mihail Davydenkov's comment deserved to be an answer:
You can also use subject.run_callbacks(:commit).
Also note that this issue (commit callbacks not getting called in transactional tests) should be fixed in rails 5.0+ so you may wish to make a note to remove any workarounds you may use in the meantime when you upgrade. See: https://github.com/rails/rails/pull/18458
Try to use test_after_commit gem
or add following code in spec/support/helpers/test_after_commit.rb - Gist
I'm using DatabaseCleaner, with a configuration where I can easily switch between transaction and truncation, where the former is preferred, because of speed, but where the latter can be used for testing callbacks.
RSpec before and after handlers work with scopes, so if you want to make truncation a scope, define a before handler;
config.before(:each, truncate: true) do
DatabaseCleaner.strategy = :truncation
end
And now to use this configuration for a describe, context or it block, you should declare it like:
describe "callbacks", truncate: true do
# all specs within this block will be using the truncation strategy
describe "#save" do
it "should trigger my callback" do
expect(lead).to receive(:send_to_SPL)
lead = Lead.create(init_hash)
end
end
end
Complete hook configuration: (store in spec/support/database_cleaner.rb)
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, truncate: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.append_after(:each) do
DatabaseCleaner.clean
end
end
Update for Rails5.
Callback handling has indeed been fixed, but you may still need to use #reload liberally.
An example:
Given a model that defines an after-create callback like so:
after_create_commit { assign_some_association }
You can spec this behavior with:
describe "callbacks" do
describe "assigning_some_association" do
subject(:saving) { record.save!; record.reload } # reload here is important
let(:record) { build(:record) }
it "assigns some association after commit" do
expect{ saving }.to(
change{ record.some_association_id }.from(nil).to(anything)
)
end
end
end
I use something like this
describe 'some method on record' do
let(:record) { create(:some_record) }
let(:update_block) { ->(record) { record.save! } } # define an labmda that will be called in a transaction block
let(:result_method) { :some_method } # define a method to be called
let(:result) do
record.class_eval <<~EVAL, __FILE__, __LINE__ + 1
after_commit :_record_result
def _record_result
#_result = public_send(:#{result_method})
end
EVAL
record.transaction do
update_block.call(record)
end
record.instance_variable_get(:'#_result')
end
before do
# apply changes to record
end
it 'returns the correct result' do
expect(result).to eq(some_value)
end
end
Related
I'm building a toy chat application using Rails 4.2.7, and am writing specs for my controllers using rspec 3.5. My Api::ChatroomsController requires a user to be logged in in order to create a chatroom, so I have created a Api::SessionsHelper module to create sessions from within the Api::ChatroomsController spec.
# app/helpers/api/sessions_helper.rb
module Api::SessionsHelper
def current_user
User.find_by_session_token(session[:session_token])
end
def create_session(user)
session[:session_token] = user.reset_session_token!
end
def destroy_session(user)
current_user.try(:reset_session_token!)
session[:session_token] = nil
end
end
# spec/controllers/api/chatrooms_controller_spec.rb
require 'rails_helper'
include Api::SessionsHelper
RSpec.describe Api::ChatroomsController, type: :controller do
before(:all) do
DatabaseCleaner.clean
User.create!({username: "test_user", password: "asdfasdf"})
end
user = User.find_by_username("test_user")
context "with valid params" do
done = false
# doesn't work if using a before(:all) hook
before(:each) do
until done do
create_session(user)
post :create, chatroom: { name: "chatroom 1" }
done = true
end
end
let(:chatroom) { Chatroom.find_by({name: "chatroom 1"}) }
let(:chatroom_member) { ChatroomMember.find_by({user_id: user.id, chatroom_id: chatroom.id}) }
it "responds with a successful status code" do
expect(response).to have_http_status(200)
end
it "creates a chatroom in the database" do
expect(chatroom).not_to eq(nil)
end
it "adds the chatroom creator to the ChatroomMember table" do
expect(chatroom_member).not_to eq(nil)
end
end
end
I'm using a before(:each) hook with a boolean variable done to achieve the behavior of a before(:all) hook for creating a single session.
If I use a before(:all) hook, I get the error:
NoMethodError: undefined method `session' for nil:NilClass`
I put a debugger in the create_session method of the Api::SessionsHelper module to check self.class and in both cases, when I use before(:each) and when I use before(:all), the class is:
RSpec::ExampleGroups::ApiChatroomsController::WithValidParams
However when using the before(:each) hook, session is {}, while in the before(:all) hook, session gives the NoMethodError above.
Anybody know what causes this error?
You need to include the helper in the test block:
RSpec.describe Api::ChatroomsController, type: :controller do
include Api::SessionsHelper
end
You can also avoid duplication by including common spec helpers in spec/rails_helper.rb
RSpec.configure do |config|
# ...
config.include Api::SessionsHelper, type: :controller
end
This is also where you should put the database_cleaner config. You should use to clean between every spec not just before all as that will lead to test ordering issues and flapping tests.
require 'capybara/rspec'
#...
RSpec.configure do |config|
config.include Api::SessionsHelper, type: :controller
config.use_transactional_fixtures = false
config.before(:suite) do
if config.use_transactional_fixtures?
raise(<<-MSG)
Delete line `config.use_transactional_fixtures = true` from rails_helper.rb
(or set it to false) to prevent uncommitted transactions being used in
JavaScript-dependent specs.
During testing, the app-under-test that the browser driver connects to
uses a different database connection to the database connection used by
the spec. The app's database connection would not be able to access
uncommitted transaction data setup over the spec's database connection.
MSG
end
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, type: :feature) do
# :rack_test driver's Rack app under test shares database connection
# with the specs, so continue to use transaction strategy for speed.
driver_shares_db_connection_with_specs = Capybara.current_driver == :rack_test
if !driver_shares_db_connection_with_specs
# Driver is probably for an external browser with an app
# under test that does *not* share a database connection with the
# specs, so use truncation strategy.
DatabaseCleaner.strategy = :truncation
end
end
config.before(:each) do
DatabaseCleaner.start
end
config.append_after(:each) do
DatabaseCleaner.clean
end
end
How can I tag an example group so that the database isn't cleaned between each example, but is cleaned before and after the whole group? And untagged specs should clean the database between each example.
I would like to write:
describe 'my_dirty_group', :dont_clean do
...
end
So in my spec_helper.rb I put:
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
end
config.before(:suite, dont_clean: true) do
DatabaseCleaner.clean
end
config.after(:suite, dont_clean: true) do
DatabaseCleaner.clean
end
config.before(:each, dont_clean: nil) do
DatabaseCleaner.start
end
config.before(:each, dont_clean: nil) do
DatabaseCleaner.clean
end
The problem is that the dont_clean: nil (or false) blocks in spec_helper don't run when the metadata tag is not specified. Is there another way to check for presence of :dont_clean before cleaning between examples?
Summary
You can set custom metadata on the whole example block, and then access the metadata within your RSpec config with self.class.metadata for use with conditional logic.
Code
Using these gem versions:
$ bundle exec gem list | grep -E '^rails |^rspec-core |^database'
database_cleaner (1.4.0)
rails (4.2.0)
rspec-core (3.2.0)
The following works for me:
File: spec/spec_helper.rb
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean_with(:truncation)
end
config.before(:all) do
# Clean before each example group if clean_as_group is set
if self.class.metadata[:clean_as_group]
DatabaseCleaner.clean
end
end
config.after(:all) do
# Clean after each example group if clean_as_group is set
if self.class.metadata[:clean_as_group]
DatabaseCleaner.clean
end
end
config.before(:each) do
# Clean before each example unless clean_as_group is set
unless self.class.metadata[:clean_as_group]
DatabaseCleaner.start
end
end
config.after(:each) do
# Clean before each example unless clean_as_group is set
unless self.class.metadata[:clean_as_group]
DatabaseCleaner.clean
end
end
end
File: spec/models/foo_spec.rb
require 'spec_helper'
describe 'a particular resource saved to the database', clean_as_group: true do
it 'should initially be empty' do
expect(Foo.count).to eq(0)
foo = Foo.create()
end
it 'should NOT get cleaned between examples within a group' do
expect(Foo.count).to eq(1)
end
end
describe 'that same resource again' do
it 'should get cleaned between example groups' do
expect(Foo.count).to eq(0)
foo = Foo.create()
end
it 'should get cleaned between examples within a group in the absence of metadata' do
expect(Foo.count).to eq(0)
end
end
You can check on example.metadata inside the blocks, although I haven't been able to figure out how to do this for before(:suite)
I have a controller sending in a list of vendors to my controller, and on normal view it's working fine.
class VendorsController < ApplicationController
respond_to :html, :json
def index
#vendor_categories = VendorCategory.where(:is_top_level => true)
#vendors = Vendor.includes(:vendor_tier, :vendor_categorizations, :vendor_categories).order('vendor_tier_id DESC, name ASC')
respond_with #vendors
end
end
In my view I have the following two lines:
= debug #vendors
= debug current_user.user_vendor_choices
which, again, are working if I view it in the browser. However, if I test it with Capybara and RSpec, it's empty.
require 'spec_helper'
describe 'Vendors' do
before do
category = create(:vendor_category)
5.times do
vendor = create(:vendor)
vendor_categorization = create(:vendor_categorization, vendor: vendor, vendor_category: category)
p vendor
p category
p vendor_categorization
end
visit signup_path
#new_user = sign_up
end
before(:each) do
visit destroy_user_session_path
visit new_user_session_path
sign_in #new_user
visit vendors_path
end
it 'should save selected vendors', js: true do
p Vendor.includes(:vendor_tier, :vendor_categorizations, :vendor_categories).order('vendor_tier_id DESC, name ASC').count
end
end
Vendor.all and the above Vendor.includes... both return values, but for some reason in my test it's not showing anything... getting a Capybara::Element not found.
UPDATE
For testing purposes, I created the Vendors directly with the controller:
def index
#vendor_categories = VendorCategory.where(:is_top_level => true)
4.times do
Vendor.create({name: 'Test McTesterson', vendor_tier_id: 1})
end
#vendors = Vendor.includes(:vendor_tier, :vendor_categorizations, :vendor_categories).order('vendor_tier_id DESC, name ASC')
respond_with #vendors
end
Spec passes. What the--? This must be a FactoryGirl issue, or for some reason my records are deleted before it can run the test? Consoling the objects after I create them is showing a record with an ID, which I guess doesn't prove that it's putting them in the database...
Turns out my Database Cleaner activities defined in my spec_helper were a little too vigorous. I had:
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
I had to get rid of the second chunk, so it now reads:
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
end
end
And it works! Not really sure why... any ideas (aside from the obvious, before it was calling database cleaner before/after each test)?
Hi I cursorily glanced at this question, not sure you even need the help anymore, but I think the reason this is failing is a fundamental set up issue that your answer is just patching around.
When you're running a js: true spec (by the way, js: true should be on the describe line, not the it line), short version, Capybara works in different threads, so instance variables created in a before block, unlike with regular Rspec testing, are not available in the spec. To make them available, you have to use a truncation cleaning strategy.
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
end
TL;DR when running a js test, truncation is basically required (unless obviously you're running js tests that don't require any database interactions). When running all other tests, use transactions (because it's also much faster). I guess your answer replicated this to some extent =)
Good day. i get this error
1) Subscription
Failure/Error: #subscription = FactoryGirl.create(:subscription)
ActiveRecord::RecordInvalid:
Encountered errors: Email already exists, Login already exists
Although in a FactoryGirl i specify the uniqueness of these fields^
call for factory girl
#subscription = FactoryGirl.create(:subscription)
build can't be implemented, because
should validate_uniqueness_of( :category_id).scoped_to(:user_id)
factories:
factory :subscription do
association :category, factory: :advt_category
user
end
factory :user do
sequence(:login) { |n| "user__#{n}" }
password "password"
sequence(:email) { |n| "example__#{n}#example.com"}
end
How to solve this error? why it appears?
Update
I ended up destroying all records for the User model in this spec
before do
User.destroy_all
end
The problem most likely is, that your test DB is not clean when you start your specs. This can happen when a rspec exits prematurely, or is killed.
Your approach with before { User.destroy_all } works, but is tedious (since you may need to add it in other specs as well), and slow.
I'd suggest you make use of the database_cleaner gem and extend your rspec configuration like this:
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.before :suite do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with :truncation
end
config.before(:each) { DatabaseCleaner.start }
config.after(:each) { DatabaseCleaner.clean }
end
When you are using Capybara for your request specs, change the before :each block to:
config.before :each do
if Capybara.current_driver == :rack_test
DatabaseCleaner.strategy = :transaction
else
DatabaseCleaner.strategy = :truncation
end
DatabaseCleaner.start
end
I think this would work
Call a FactoryGirl for user and pass it into the subscription call
#user = FactoryGirl.create(:user)
#subscription = FactoryGirl.create(:subscription, :user_id => #user.id)
As of why this happends we need to see more of the spec
How do I turn off transactional fixtures for only one spec (or Steak scenario) with RSpec 2?
I tried some things found on the web without any success.
This leads to an undefined method exception.
describe "MyClass without transactional fixtures" do
self.use_transactional_fixtures = false
...
end
This simply does nothing (transactional fixture is still on):
describe "MyClass without transactional fixtures" do
RSpec.configure do |config|
config.use_transactional_fixtures = false
end
...
end
What else could I try?
I usually add a helper like this:
def without_transactional_fixtures(&block)
self.use_transactional_fixtures = false
before(:all) do
DatabaseCleaner.strategy = :truncation
end
yield
after(:all) do
DatabaseCleaner.strategy = :transaction
end
end
Which lets me turn off transactional fixtures for a specific block in the specs:
describe "doing my thing" do
without_transactional_fixtures do
it "does something without transaction fixtures" do
...
end
end
end
I've did it this way, with database_cleaner, in order to test code that uses transactions (which will conflict with transactional_fixtures or any other strategy to make transactional tests e.g. DatabaseCleaner.strategy = :truncation or :transaction):
# spec_helper.rb
config.use_transactional_fixtures = false
config.around(:each, :testing_transactions => true) do |ex|
DatabaseCleaner.strategy = nil
ex.run
DatabaseCleaner.strategy = :truncation
end
and in my test cases:
it "should not save if one of objects are invalid", :testing_transactions => true
This used to be a bug (see ticket #197), but I seems to be okay now. I just don't know if it will work on a per test base (probably not). If you want to do this, you can disable transactional fixtures globally by putting config.use_transactional_fixtures = false on the spec_helper.rb and use DatabaseCleaner to set that.
I've had a similar problem when testing pages with javascript on the browser (a scenario that does not work with transactional fixtures). Here's how I managed to work around it: http://github.com/lailsonbm/contact_manager_app
I mixed both answers and it worked for me on RSpec 3:
config.around(:each, use_transactional_fixtures: false) do |example|
self.use_transactional_fixtures = false
example.run
self.use_transactional_fixtures = true
DatabaseCleaner.clean_with(:deletion)
end
You can then use it in the describe, context or it block
describe 'my test', use_transactional_fixtures: false do
...
end
Not sure if that applies to RSpec2, but works fine with 3.
config.use_transactional_fixtures = true
config.around(:each, use_transactional_fixtures: false) do |example|
self.use_transactional_tests = false
example.run
self.use_transactional_tests = true
end
Mind the use_transactional_fixtures (rspec-rails option) and use_transactional_tests (activerecord fixtures option) difference.
Use use_transactional_tests instead of use_transactional_fixtures When Rspec 2.3.8 is being used
def without_transactional_fixtures(&block)
self.use_transactional_tests = false
before(:all) do
DatabaseCleaner.strategy = :truncation
end
yield
after(:all) do
DatabaseCleaner.strategy = :transaction
end
self.use_transactional_tests = true
end
The 2023 answer for RSpec 6.0:
uses_transaction "doesn't run in transaction"
it "doesn't run in transaction" do
expect(ActiveRecord::Base.connection.transaction_open?).to eq(false)
end
it "runs in transaction" do
expect(ActiveRecord::Base.connection.transaction_open?).to eq(true)
end
https://github.com/rspec/rspec-rails/blob/v6.0.1/spec/rspec/rails/fixture_support_spec.rb#L21