I have the following controller:
def create
#order = Order.create(order_params)
OrderProcessorWorker.perform_async(#order.id)
end
And the following worker:
def perform(order_id)
#.. some logic
#order.status = 'processed'
#order.save
#.. some other logic
end
My question is... What is the correct way of testing that the job is changing the status of the Order? This gem https://github.com/philostler/rspec-sidekiq helps with testing that the job enqueued, delayed, retried...etc. But how do you test what is the job is actually doing?
EDIT: Here is what I have done:
describe OrderProcessorWorker do
describe '#perform' do
context 'an order without any pages' do
it 'changes the status of the order to missing_pages' do
FactoryGirl.create(:order_processing_status)
p OrderProcessingStatus.all # => This shows the just created order_processing_status
order = FactoryGirl.create(:order)
subject.perform(order.id)
end
end
end
end
What happens is that, the perform method that is being called is not the one defined. I have tried doing p 'hello worlds' and it does not show in there.
To test that your worker is doing the work you intend it to do - test perform rather than perform_async:
describe OrderProcessorWorker do
it 'changes order status' do
subject.perform(order_id)
expect(Order.find(id: order_id).status).to eq 'processed'
end
end
If you are testing create:
assert_equal 0, OrderProcessorWorker.jobs.size
post :create, ...
assert_equal 1, OrderProcessorWorker.jobs.size
Related
Can somebody help me with rspec testing method call in Service Object?
class UserEntitiesController < ApplicationController
def create
#result = UserEntities::Create.new(params).call
return render '/422.json.jbuilder', status: :unprocessable_entity unless #result
end
here is the service objects:
module UserEntities
class Create
attr_accessor :params
def initialize(params)
#params = params
end
def call
#user_entity = UserEntity.new(user_entity_params)
set_time
if #user_entity.save
#user_entity
else
error_result
end
end
private
def error_result
false
end
def user_entity_params
#params.require(:user_entity).permit(:information,
:destroy_option,
:reviews)
end
def set_time
if #params[:available_days].present?
#user_entity.termination = Time.now + #params[:available_days].days
end
end
end
end
I tried to find information how to do this, but there are not so many.
Also i read some
You can certainly write a unit test to test the Service Object standalone
In this case, create a file spec/services/user_entities/create_spec.rb
describe UserEntities::Create do
let(:params) { #values go here }
context ".call" do
it "create users" do
UserEntities::Create.new(params).call
# more test code
end
# more tests
end
end
Later in the controller tests, if you are planning to write such, you do not need to test UserEntities::Create instead you can just mock the service object to return the desired result
describe UserEntitiesController do
before do
# to mock service object in controller test
allow(UserEntities::Create).to receive(:new)
.and_return(double(:UserEntities, call: "Some Value"))
end
# controller tests go here
end
As a supplement to #bibin answer.
If you want to mock some instance's method renturn:
allow_any_instance_of(UserEntities::Create).to receive(:call).and_return("some value")
if you want to raise a eror:
allow_any_instance_of(UserEntities::Create).to receive(:call).and_raise("boom")
I have my controller which I'm trying to test.
class ShortLinksController < ApplicationController
after_action :increment_view_count, only: :redirect_to_original_url
def redirect_to_original_url
link = ShortLink.find(params[:short_url])
redirect_to "http://#{link.original_url}"
end
private
def increment_view_count
ShortLink.increment_counter(:view_count, params[:short_url])
end
end
This is the route for redirect_to_original_url:
get 's/:short_url', to: 'short_links#redirect_to_original_url', as: 'redirect_to_original_url'
And my Rspec tests:
describe "#redirect_to_original_url" do
let(:short_link) {ShortLink.create(original_url: 'www.google.com')}
subject {get :redirect_to_original_url, params: {short_url: short_link.id}}
it 'should increment the count by 1 original url is visited' do
expect {subject}.to change{ short_link.view_count }.by(1)
end
end
For some reason I get the following error when I run my tests:
expected `short_link.view_count` to have changed by 1, but was changed by 0
My logic works as I can see it increments that individual link view_count by 1, but not my test.
Check the default value of view_count when you create a object for ShortLink model using,
let(:short_link) {ShortLink.create(original_url: 'www.google.com')}
//Creating object
it 'should have value 0 when shortlink object is created' do
expect(short_link.view_count).to eq(0)
end
If this example fails then create object with default value for view_count as,
let(:short_link) {ShortLink.create(original_url: 'www.gmail.com',view_count: 0)}
Meanwhile as Jake Worth said your rspec test is not calling,
after_action :increment_view_count, only: :redirect_to_original_url
in your controller(check this by calling increment_view_count function from your redirect_to_original_url function and run your tests).
Since you created short_link variable you need to reload it to check that value was changed. Unless reloading it stores previous value.
expect { subject }.to change{ short_link.reload.view_count }.by(1)
I am looking through some tests written by other people and i keep seeing ':where' in their tests. I guess its a stub, but just finding my feet with testing and want to know is it any different from a normal stubs, what does the naming imply?
describe "delete destroy" do
context "is not allowed by user" do
before do
allow(model).to receive(:where).and_return(no_instances)
allow(no_instances).to receive(:first).and_return(no_instances)
end
context "where the record is destroyed" do
before do
allow(instance).to receive(:destroy).and_return(true)
delete :destroy, params_id
end
sets_flash(:notice)
redirects_to('/airbrake_accounts')
end
context "where the record is not destroyed" do
before do
allow(instance).to receive(:destroy).and_return(false)
delete :destroy, params_id
end
sets_flash(:error)
redirects_to('/airbrake_accounts')
end
end
context "where the record is not found" do
before do
allow(model).to receive(:where).and_return(no_instances)
delete :destroy, params_id
end
sets_flash(:error)
redirects_to('/airbrake_accounts')
end
end
I can see what is going on here (I think), things like ':new' are controller actions right?
describe "photo create" do
before do
allow(model).to receive(:new).and_return(instance)
end
context "where all is not well" do
before do
allow(instance).to receive(:save).and_return(false)
post :create, params_new_instance
end
sets_flash(:error)
it "should render the new form" do
expect(response).to render_template("entries/new")
end
end
context "where all is well" do
before do
allow(instance).to receive(:save).and_return(true)
post :create, params_new_photo
end
sets_flash(:notice)
redirects_to ('/photos')
end
end
They are class or instance methods on the model. Lets just say that the model variable in your example is set to the Dog model and this is testing the DogController.
# model
class Dog
def where(params)
do_stuff
end
end
# controller
class DogController > ApplicationController
def destroy
#dogs = Dog.where(id: 1)
redirect :new
end
end
Now I want to test whats going on in my controller, but I don't want to test anything that my model actually does. I'm isolating a unit of my code to be tested. This is different than testing how it all works integrated together (google unit testing or integration testing).
In order to test just whats going on in my controller I stub the methods happening on my model to keep things isolated and clean. The way I do this is by stubbing it out in my controller spec. So In my DogControllerSpec I do:
before do
allow(Dog).to receive(:where).and_return([])
end
So I'm saying allow my dog class to receive the 'where' method call, but don't execute its logic, and instead return me an empty array.
Then I can setup up the code I'm actually testing which is that my destroy method renders new when called.
it "should render the new form" do
expect(response).to render_template("dogs/new")
end
The key here is really that you are attempting to decouple your tests, so that when you unit test your DogController#destroy you are not testing your Dog.where method. The reasoning being that if you change code in your Dog model it should not break specs in your DogControllerSpec.
They are using symbol #to_proc, I believe, to 'stub' calling the method #where on model. So that means the line:
allow(model).to receive(:where).and_return(no_instances)
is essentially
model.where #=> [].
:new is a controller action, but the way this code is using it is more like SomeClass.new, i.e. the method to create an instance of a class(which is the model).
What I'm doing
I recently implemented multi-tenancy (using scopes) following Multitenancy with Scopes (subscription required) as a guide. NOTE: I am using the dreaded "default_scope" for tenant scoping (as shown in Ryan's Railscast). Everything is working in browser just fine, but many (not all) of my tests are failing and I can't figure out why.
I built authentication from scratch (based on this Railscast: Authentication from Scratch (revised) - subscription required) and using an auth_token for "Remember me" functionality (based on this Railscast: Remember Me & Reset Password).
My question
Why is this test failing, and why do the two workarounds work? I've been stumped for a couple days now and just can't figure it out.
What I think is happening
I'm calling the Jobs#create action, and the Job.count is reducing by 1 instead of increasing by 1. I think what's happening is the job is being created, then the app is losing the 'tenant' assignment (tenant is dropping to nil), and the test is counting Jobs for the wrong tenant.
What's odd is that it's expecting "1" and getting "-1" (and not "0"), which implies it's getting a count (note that there's already a 'seed' job created in the before block, so it's probably counting "1" before calling #create), calling the create action (which should increase the count by 1 to 2 total), then losing the tenant and switching to a nil tenant where there are 0 jobs. So it:
Counts 1 (seed job)
Creates a job
Loses the tenant
Counts 0 jobs in the new (probably nil) tenant
...resulting in a -1 change in the Job.count.
You can see below that I've semi-confirmed this by adding ".unscoped" to my Job.count line in the test. This implies that the expected number of jobs is there, but the jobs just aren't in the tenant the app is testing under.
What I don't understand is how it's losing the tenant.
Code
I've tried to grab the relevant parts of my code, and I've created a dedicated single-test spec to make this as easy to dissect as possible. If I can do anything else to make this easy on possible answerers, just let me know what to do!
# application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
around_filter :scope_current_tenant
private
def current_user
#current_user ||= User.unscoped.find_by_auth_token!(cookies[:auth_token]) if cookies[:auth_token]
end
helper_method :current_user
def current_tenant
#current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
end
helper_method :current_tenant
def update_current_tenant
Tenant.current_id = current_tenant.id if current_tenant
end
helper_method :set_current_tenant
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil
end
end
# sessions_controller.rb
class SessionsController < ApplicationController
def create
user = User.unscoped.authenticate(params[:session][:email], params[:session][:password])
if user && user.active? && user.active_tenants.any?
if params[:remember_me]
cookies.permanent[:auth_token] = user.auth_token
else
cookies[:auth_token] = user.auth_token
end
if !user.default_tenant_id.nil? && (default_tenant = Tenant.find(user.default_tenant_id)) && default_tenant.active
# The user has a default tenant set, and that tenant is active
session[:tenant_id] = default_tenant.id
else
# The user doesn't have a default
session[:tenant_id] = user.active_tenants.first.id
end
redirect_back_or root_path
else
flash.now[:error] = "Invalid email/password combination."
#title = "Sign in"
render 'new'
end
end
def destroy
cookies.delete(:auth_token)
session[:tenant_id] = nil
redirect_to root_path
end
end
# jobs_controller.rb
class JobsController < ApplicationController
before_filter :authenticate_admin
# POST /jobs
# POST /jobs.json
def create
#job = Job.new(params[:job])
#job.creator = current_user
respond_to do |format|
if #job.save
format.html { redirect_to #job, notice: 'Job successfully created.' }
format.json { render json: #job, status: :created, location: #job }
else
flash.now[:error] = 'There was a problem creating the Job.'
format.html { render action: "new" }
format.json { render json: #job.errors, status: :unprocessable_entity }
end
end
end
end
# job.rb
class Job < ActiveRecord::Base
has_ancestry
default_scope { where(tenant_id: Tenant.current_id) }
.
.
.
end
# sessions_helper.rb
module SessionsHelper
require 'bcrypt'
def authenticate_admin
deny_access unless admin_signed_in?
end
def deny_access
store_location
redirect_to signin_path, :notice => "Please sign in to access this page."
end
private
def store_location
session[:return_to] = request.fullpath
end
end
# spec_test_helper.rb
module SpecTestHelper
def test_sign_in(user)
request.cookies[:auth_token] = user.auth_token
session[:tenant_id] = user.default_tenant_id
current_user = user
#current_user = user
end
def current_tenant
#current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
end
end
# test_jobs_controller_spec.rb
require 'spec_helper'
describe JobsController do
before do
# This is all just setup to support requirements that the admin is an "Admin" (role)
# That there's a tenant for him to use
# That there are some workdays - a basic requirement for the app - jobs, checklist
# All of this is to satisfy assocations and
#role = FactoryGirl.create(:role)
#role.name = "Admin"
#role.save
#tenant1 = FactoryGirl.create(:tenant)
#tenant2 = FactoryGirl.create(:tenant)
#tenant3 = FactoryGirl.create(:tenant)
Tenant.current_id = #tenant1.id
#user = FactoryGirl.create(:user)
#workday1 = FactoryGirl.create(:workday)
#workday1.name = Time.now.to_date.strftime("%A")
#workday1.save
#checklist1 = FactoryGirl.create(:checklist)
#job = FactoryGirl.create(:job)
#checklist1.jobs << #job
#workday1.checklists << #checklist1
#admin1 = FactoryGirl.create(:user)
#admin1.tenants << #tenant1
#admin1.roles << #role
#admin1.default_tenant_id = #tenant1.id
#admin1.pin = ""
#admin1.save!
# This is above in the spec_test_helper.rb code
test_sign_in(#admin1)
end
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{ # <-- This is line 33 that's mentioned in the failure below
post :create, job: FactoryGirl.attributes_for(:job)
# This will pass if I change the below to Job.unscoped
# OR it will pass if I add Tenant.current_id = #tenant1.id right here.
# But I shouldn't need to do either of those because
# The tenant should be set by the around_filter in application_controller.rb
# And the default_scope for Job should handle scoping
}.to change(Job,:count).by(1)
end
end
end
end
Here is the failure from rspec:
Failures:
1) JobsController POST create with valid attributes creates a new job
Failure/Error: expect{
count should have been changed by 1, but was changed by -1
# ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.66481 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job
If I add some 'puts' lines to see who the current_tenant is directly and by inspecting the session hash, I see the same tenant ID all the way:
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{
puts current_tenant.id.to_s
puts session[:tenant_id]
post :create, job: FactoryGirl.attributes_for(:job)
puts current_tenant.id.to_s
puts session[:tenant_id]
}.to change(Job,:count).by(1)
end
end
end
Yields...
87
87
87
87
F
Failures:
1) JobsController POST create with valid attributes creates a new job
Failure/Error: expect{
count should have been changed by 1, but was changed by -1
# ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.66581 seconds
1 example, 1 failure
Failed examples:
rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job
I think it's not that RSpec is ignoring the default scope but it's reset in the ApplicationController in the around filter by setting the current user to nil.
I encountered this issue with assigns(...) and it happened because the relation is actually resolved when you're evaluating assigns. I think this may also be the case with the expectation in your case.
UPDATE: In my situation, the cleanest solution I could find (though I still hate it) is to let the default scope leak through by not setting the current user to nil in test environment.
In your case this would amount to:
def scope_current_tenant
update_current_tenant
yield
ensure
Tenant.current_id = nil unless Rails.env == 'test'
end
I haven't tested it with your code but maybe this will help.
I managed to get my tests to pass, although I'm still not sure why they were failing to begin with. Here's what I did:
describe "POST create" do
context "with valid attributes" do
it "creates a new job" do
expect{ # <-- This is line 33 that's mentioned in the failure below
post :create, job: FactoryGirl.attributes_for(:job)
}.to change(Job.where(tenant_id: #tenant1.id),:count).by(1)
end
end
end
I changed:
change(Job,:count).by(1)
...to:
change(Job.where(tenant_id: #tenant1.id),:count).by(1)
NOTE: #tenant1 is the logged-in admin's tenant.
I assumed default_scopes would be applied in RSpec, but it seems they aren't (or at least not in the ":change" portion of an "expect" block). In this case, the default_scope for Job is:
default_scope { where(tenant_id: Tenant.current_id) }
In fact, if I change that line to:
change(Job.where(tenant_id: Tenant.current_id),:count).by(1)
...it will also pass. So if I explicitly mimic the default_scope for Job within the spec, it'll pass. This seems like confirmation that RSpec is ignoring my default_scope on Jobs.
In a way, I think my new test is a better way to make sure tenant data stays segregated because I'm explicitly checking counts within a particular tenant rather than implicitly checking the counts for a tenant (by assuming the count is in the "current tenant").
I'm marking my answer is correct because it's the only answer, and if someone else encounters this, I think my answer will help them get past the issue. That said, I really haven't answered my original question regarding why the test was failing. If anyone has any insight into why RSpec seems to be ignoring default_scope in "expect" blocks, that might help making this question useful for others.
I have the same issue of you guys. I didn't resolve in a way that makes me comfortable but is still better than verifying your RAILS_ENV. Take this example.
it "saves person" do
expect {
some_post_action
}.to change(Person, :count).by(1)
end
Every time i try to save the count method makes a select like:
"select count(*) from persons where tenant_id is null"
I manage to resolve this issue by setting Person.unscoped in the change method i changed this:
}.to change(Person, :count).by(1)
to this:
}.to change(Person.unscoped, :count).by(1)
It's not the best solution but i'm still trying to find a way to get around the default_scope.
Given a controller like this where it creates several instance variables for use by the view, would you generally test that each of those get set properly? It seems like you would want to, but it also seems a little it could be a bit tricky. What's the right approach?
class StaffsController < ApplicationController
def index
set_index_vars
#all_staff = Staff.find_staff_for_business_all_inclusive(current_business_id)
respond_to do |format|
format.html { render :action => "index", :locals => { :all_staff => #all_staff, :all_services => #all_services, :new_vacation => #new_vacation } }
end
end
def set_index_vars
#days_of_week = days_of_week
#first_day_of_week = DefaultsConfig.first_day_of_week
#all_services = Service.services_for_business(current_business_id)
#new_vacation = StaffVacation.new
#has_hit_staff_limit = current_user_plan.has_hit_staff_limit?
end
end
The code is also posted at https://gist.github.com/1018190
If you're going to write a controller spec, then yes, by all means test that the instance variables are assigned. Much of the 'trickiness' can come from dependencies on other models/libraries, so stub out those method calls:
# air code
Staff.stub(:find_staff_for_business_all_inclusive) {array_of_staff}
controller.stub(:days_of_week) {['Monday','Tuesday',....etc...]}
DefaultsConfig.stub(:first_day_of_week) {"Monday"}
Service.stub(:services_for_business).with(some_value_for_the_current_business_id).\
and_return(some_relevant_value)
StaffVacation.stub(:new) {something_meaningful}
controller.stub_chain(:current_user_plan,:has_hit_staff_limit?) {false}
get :index
assigns(:days_of_week).should == ['Monday','Tuesday',....etc...]
# ...etc...
I would split it up as follows: test that the index calls the correct method. Then test whether the method works.
So something like
describe StaffsController do
describe "GET #index" do
it "calls set_index_vars" do
controller.should_receive(:set_index_vars)
get :index
end
# and your usual tests ...
end
describe "#set_index_vars" do
before(:each) do
# stub out the code not from this controller
controller.stub_chain(:current_user_plan, :has_hit_staff_limit?).and_return(false)
.. etc ..
controller.set_index_vars
end
it { assigns(:days_of_week).should == controller.days_of_week }
it { assigns(:has_hit_staff_limit).should be_false
# etc ..
end
end
Hope this helps.
So long as you have good coverage around your method, you can test that your method is being called at the right times, with the right values etc. Something like:
describe StaffsController do
describe "GET #index" do
it "should call set_index_vars" do
controller.should_receive(:set_index_vars)
get :index
end
end
describe "#set_index_vars" do
it "should assign instance variables with correct values" do
# or wtv this is supposed to do
get :index
assigns(:days_of_week).should == controller.days_of_week
# etc ..
end
end
end