I have a problem with testing one of my workers in rails app. It looks like this:
class UserStatisticsWorker
include Sidekiq::Worker
include Sidetiq::Schedulable
def perform(administration_id = nil)
administrations(administration_id).find_each do |administration|
User::StatisticsCalculator.new.recalculate_if_needed(administration.id)
end
end
private
def administrations(administration_id = nil)
administration_id.present? ? Administration.where(id: administration_id) : Administration.all
end
end
And it is tested with rspec:
require 'spec_helper'
describe UserStatisticsWorker do
describe 'perform' do
let!(:administration) { create(:administration) }
let!(:administration_2) { create(:administration) }
context 'when administration_id is present' do
it 'runs User::StatisticsCalculator for one administration' do
expect_any_instance_of(User::StatisticsCalculator).to receive(:recalculate_if_needed).once
subject.perform(administration.id)
end
end
context 'when administration_id is not present' do
it 'runs User::StatisticsCalculator for all administrations' do
expect_any_instance_of(User::StatisticsCalculator).to receive(:recalculate_if_needed).twice
subject.perform
end
end
end
end
The second spec is not pass with following error:
The message 'recalculate_if_needed' was received by #<User::StatisticsCalculator:85721520 > but has already been received by #<User::StatisticsCalculator:0x0000000a383498>
Why is that?
A very good practice is to avoid any_instance_of and instead extract private methods in your worker which can be more easily tested. A refactor would look something like this:
class UserStatisticsWorker
include Sidekiq::Worker
include Sidetiq::Schedulable
def perform(administration_id = nil)
administrations(administration_id).find_each do |administration|
recalculate_if_needed(administration)
end
end
private
def recalculate_if_needed(administration)
User::StatisticsCalculator.new.recalculate_if_needed(administration.id)
end
def administrations(administration_id = nil)
administration_id.present? ? Administration.where(id: administration_id) : Administration.all
end
end
Then, test it like this:
require 'spec_helper'
describe UserStatisticsWorker do
describe 'perform' do
let!(:administration) { create(:administration) }
let!(:other_administration) { create(:administration) }
context 'when administration_id is present' do
it 'tries to recalculate for the specific administration' do
expect(subject).to receive(:recalculate_if_needed).once
subject.perform(administration.id)
end
end
context 'when administration_id is not present' do
it 'tries to recalculate for all administrations' do
expect(subject).to receive(:recalculate_if_needed).twice
subject.perform
end
end
end
end
The problem is that you have set the expectation to happen twice on an instance... but what is actually happening is that it is being called Once on two different instances.
ie this is not the expectation that you're looking for...
see the other answer for what you could try instead.
Related
I have a service and I want to test that a function is called. I'm not sure how to test it because it doesn't seem like there is a subject that's being acted on.
class HubspotFormSubmissionService
def initialize(form_data)
#form_data = form_data
end
def call
potential_client = createPotentialClient
end
def createPotentialClient
p "Step 1: Attempting to save potential client to database"
end
end
I want to test that createPotentialClient is called:
require 'rails_helper'
RSpec.describe HubspotFormSubmissionService, type: :model do
describe '#call' do
let(:form_data) { {
"first_name"=>"Jeremy",
"message"=>"wqffew",
"referrer"=>"Another Client"
} }
it 'attempts to process the form data' do
expect(HubspotFormSubmissionService).to receive(:createPotentialClient)
HubspotFormSubmissionService.new(form_data).call
end
end
end
What should I be doing differently?
You can just set the subject like this. Then in the test expect subject to receive the method like you have after it is mocked. I would also have a separate test for createPotentialClient to test that it is returning the value you expect.
subject { described_class.call }
before do
allow(described_class).to receive(:createPotentialClient)
end
it 'calls the method' do
expect(described_class).to receive(:createPotentialClient)
subject
end
There is a private method with the following code.
attr_reader :some_variable
validate :some_def
def some_def
unless some_variable.valid?
some_variable.errors.messages.each do |message|
errors.add(:some_variable, message)
end
end
I am new to rspec and not familiar with private method testing. Any help is appreciated.
I need to cover the lines of the private method.
you can do something like this:
describe 'validations' do
let(:some_variable_object) { SomeVariable.new }
let(:new_foo) { described_class.new(some_variable: some_variable_object) }
context 'when some_variable is valid' do
before do
allow(some_variable_object).to receive(:valid?) { true }
end
it 'is valid' do
expect(new_foo).to be_valid
end
it 'does not have errors related to some_variable' do
expect(new_foo.errors[:some_variables]).to be_empty
end
end
then you can do the same to test the opposite, when some_variable is not valid...
now, there are tools to help you setting up objects within the spec easily (FactoryBot).
Im writing a test for this service.
def run
sort_offers(product).each do |product_code|
......
offer.update(poduct_params)
Importer::Partner.get_details(product_code).new
end
end
It's calling a service which in some cases will override the values that were saved when running offer.update(product_prams). How would I go about skipping the service call within my test?
Here is the example of my test
context 'is valid' do
.... .....
before do
Importer::ProductCodes(product).run
end
it ......
end
I would stub Importer::Partner.get_details to return a double that responds to new:
context 'is valid' do
before do
allow(Importer::Partner).to receive(:get_details).and_return(double(new: nil))
end
# it ...
end
Depending on your needs you might want to add an expectation that the mock was called with the correct parameters and that new was actually called on the mock too:
context 'is valid' do
let(:mock) { double(new: nil) }
before do
allow(Importer::Partner).to receive(:get_details).and_return(double(new: nil))
end
it "calls the service" do
an_instance.run
expect(Importer::Partner).to have_received(:get_details).with(
foo: 'bar' # the arguments you would expect
)
expect(mock).to have_received(:new)
end
end
RSpec has a very capable stubbing and mocking library built in (rspec mocks).
require 'spec_helper'
module Importer
class Partner
def self.get_details(product_code)
"original return value"
end
end
end
class FooService
def self.run
Importer::Partner.get_details('bar')
end
end
RSpec.describe FooService do
let(:partner_double) { class_double("Importer::Partner") }
before do
stub_const("Importer::Partner", partner_double)
allow(partner_double).to receive(:get_details).and_return 'our mocked value'
end
it "creates a double for the dependency" do
expect(FooService.run).to eq 'our mocked value'
end
end
class_double creates a double for the class and you can set the return values by using .expect and .allow and the mocking interface. This is quite useful since you can stub out the new or intialize methods to return a double or spy.
stub_constant will reset the constant to its previous value when the spec is done.
That said you can avoid the use of stub_constant by using constructor injection in your services:
class PhotoImportService
attr_accessor :client, :username
def initialize(username, api_client: nil)
#username = username
#client = api_client || APIClient.new(ENV.fetch('API_KEY'))
end
def run
client.get_photos(username)
end
end
i am using rails and want to write a test for password reset in Rspec. i am quite new to testing.
this is what i have done so far:
require 'rails_helper'
describe UsersController, type: :controller do
describe 'post #reset_password' do
let(:user) { create(:user) }
context "reset password" do
def do_request
patch :update_password
end
before { do_request }
it { expect(ActionMailer::Base.deliveries.count(1) }
end
end
end
every time i run this it gives ma an syntax error in
"it { expect(ActionMailer::Base.deliveries.count(1) } ".
i want to check whether the email successfully sent of not and if the user have key in the email.
Thanks!
1) you miss ) at last here so got syntax error
it { expect(ActionMailer::Base.deliveries.count(1) }
to
it { expect(ActionMailer::Base.deliveries.count(1)) }
2)
If you want to check total deliveries. you can try
it 'should send an email' do
ActionMailer::Base.deliveries.count.should == 1
end
also check sender
it 'renders the sender email' do
ActionMailer::Base.deliveries.first.from.should == ['notifications#domain.com']
end
Also check subject line
it 'should set the subject to the correct subject' do
ActionMailer::Base.deliveries.first.subject.should == 'Here Is Your Story!'
end
The problems you're having will most likely be fixed by writing better tests.
Here's generally how you would write tests for something like this.
Lets suppose in your routes file you have a post route that looks something like this
# config/routes.rb
post "/user/:id/reset_password", to: "users#reset_password"
And your User controller looks something like this
# app/controllers/users_controller.rb
class UsersController
...
def reset_password
user = User.find(params[:id])
user.reset_password!
SomeMailClass.email_reset_instructions(user)
end
end
and your User.rb model looks something like this
# app/models/user.rb
class User < ActiveRecord::Base
def reset_password!
update!(password: nil) # or whatever way you want/need to reset the password
end
end
and you have some type of mailing class to send your email
# app/models/some_mail_class.rb
class SomeMailClass
def self.email_reset_instructions(user)
# do something to send email...
end
end
The way you would go about testing this in the controller would be
# spec/controllers/users_controller_spec.rb
require 'rails_helper'
describe UsersController, type: :controller do
it "#reset_password" do
user_id = double(:user_id)
user = double(:user)
expect(User).to receive(:find).with(user_id).and_return(user)
expect(user).to receive(:reset_password!).and_return(true)
expect(SomeMailClass).to receive(:email_reset_instructions).with(user)
post :reset_password, id: user_id
end
end
But you shouldn't stop there. Because the implementation of the newly made method reset_password! and the SomeMailClass has yet to be tested. So you would write model/unit tests like this for them
# spec/models/user_spec.rb
require "rails_helper"
describe User do
it ".reset_password!" do
user = User.create(password: "foo")
expect(user.password).to eq "foo"
user.reset_password!
expect(user.password).to eq nil
end
end
Then you might install vcr and factory_girl gems and use them like so to test your mailer
# spec/models/some_mail_class_spec.rb
require "rails_helper"
describe SomeMailClass do
VCR.use_cassette "email_reset_instructions" do |cassette|
it ".email_reset_instructions" do
user = FactoryGirl.create(:user)
SomeMailClass.email_reset_instructions(user)
# you can write some expectations on the cassette obj to test.
# or you can write whatever expectations you need/desire
end
end
end
And in the end if there was something happening on the front end that a user would click that made this post request you would write a feature test for it as well.
Hope this helps!
I have the following (simplified) Rails Concern:
module HasTerms
extend ActiveSupport::Concern
module ClassMethods
def optional_agreement
# Attributes
#----------------------------------------------------------------------------
attr_accessible :agrees_to_terms
end
def required_agreement
# Attributes
#----------------------------------------------------------------------------
attr_accessible :agrees_to_terms
# Validations
#----------------------------------------------------------------------------
validates :agrees_to_terms, :acceptance => true, :allow_nil => :false, :on => :create
end
end
end
I can't figure out a good way to test this module in RSpec however - if I just create a dummy class, I get active record errors when I try to check that the validations are working. Has anyone else faced this problem?
Check out RSpec shared examples.
This way you can write the following:
# spec/support/has_terms_tests.rb
shared_examples "has terms" do
# Your tests here
end
# spec/wherever/has_terms_spec.rb
module TestTemps
class HasTermsDouble
include ActiveModel::Validations
include HasTerms
end
end
describe HasTerms do
context "when included in a class" do
subject(:with_terms) { TestTemps::HasTermsDouble.new }
it_behaves_like "has terms"
end
end
# spec/model/contract_spec.rb
describe Contract do
it_behaves_like "has terms"
end
You could just test the module implicitly by leaving your tests in the classes that include this module. Alternatively, you can include other requisite modules in your dummy class. For instance, the validates methods in AR models are provided by ActiveModel::Validations. So, for your tests:
class DummyClass
include ActiveModel::Validations
include HasTerms
end
There may be other modules you need to bring in based on dependencies you implicitly rely on in your HasTerms module.
I was struggling with this myself and conjured up the following solution, which is much like rossta's idea but uses an anonymous class instead:
it 'validates terms' do
dummy_class = Class.new do
include ActiveModel::Validations
include HasTerms
attr_accessor :agrees_to_terms
def self.model_name
ActiveModel::Name.new(self, nil, "dummy")
end
end
dummy = dummy_class.new
dummy.should_not be_valid
end
Here is another example (using Factorygirl's "create" method" and shared_examples_for)
concern spec
#spec/support/concerns/commentable_spec
require 'spec_helper'
shared_examples_for 'commentable' do
let (:model) { create ( described_class.to_s.underscore ) }
let (:user) { create (:user) }
it 'has comments' do
expect { model.comments }.to_not raise_error
end
it 'comment method returns Comment object as association' do
model.comment(user, "description")
expect(model.comments.length).to eq(1)
end
it 'user can make multiple comments' do
model.comment(user, "description")
model.comment(user, "description")
expect(model.comments.length).to eq(2)
end
end
commentable concern
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
def comment(user, description)
Comment.create(commentable_id: self.id,
commentable_type: self.class.name,
user_id: user.id,
description: description
)
end
end
and restraunt_spec may look something like this (I'm not Rspec guru so don't think that my way of writing specs is good - the most important thing is at the beginning):
require 'rails_helper'
RSpec.describe Restraunt, type: :model do
it_behaves_like 'commentable'
describe 'with valid data' do
let (:restraunt) { create(:restraunt) }
it 'has valid factory' do
expect(restraunt).to be_valid
end
it 'has many comments' do
expect { restraunt.comments }.to_not raise_error
end
end
describe 'with invalid data' do
it 'is invalid without a name' do
restraunt = build(:restraunt, name: nil)
restraunt.save
expect(restraunt.errors[:name].length).to eq(1)
end
it 'is invalid without description' do
restraunt = build(:restraunt, description: nil)
restraunt.save
expect(restraunt.errors[:description].length).to eq(1)
end
it 'is invalid without location' do
restraunt = build(:restraunt, location: nil)
restraunt.save
expect(restraunt.errors[:location].length).to eq(1)
end
it 'does not allow duplicated name' do
restraunt = create(:restraunt, name: 'test_name')
restraunt2 = build(:restraunt, name: 'test_name')
restraunt2.save
expect(restraunt2.errors[:name].length).to eq(1)
end
end
end
Building on Aaron K's excellent answer here, there are some nice tricks you can use with described_class that RSpec provides to make your methods ubiquitous and make factories work for you. Here's a snippet of a shared example I recently made for an application:
shared_examples 'token authenticatable' do
describe '.find_by_authentication_token' do
context 'valid token' do
it 'finds correct user' do
class_symbol = described_class.name.underscore
item = create(class_symbol, :authentication_token)
create(class_symbol, :authentication_token)
item_found = described_class.find_by_authentication_token(
item.authentication_token
)
expect(item_found).to eq item
end
end
context 'nil token' do
it 'returns nil' do
class_symbol = described_class.name.underscore
create(class_symbol)
item_found = described_class.find_by_authentication_token(nil)
expect(item_found).to be_nil
end
end
end
end