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 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.
I am using Ruby on Rails 3.0.9 and RSpect 2. I am trying to refactoring some spec file in the following way (in order to test with less code similar User class object attribute values):
describe User do
let(:user1) { Factory(:user, :users_attribute_a => 'invalid_value') }
let(:user2) { Factory(:user, :users_attribute_b => 'invalid_value') }
let(:user3) { Factory(:user, :users_attribute_c => 'invalid_value') }
it "foreach user" do
[ user1, user2, user3 ].each do |user|
subject { user }
it "should be whatever"
user.should_not be_valid
...
end
end
end
end
However, if I run the above test I get the following error:
Failure/Error: it "should be whatever" do
NoMethodError:
undefined method `it' for #<RSpec::Core::ExampleGroup::Nested_1::Nested_2::Nested_2:0x00000106ccee60>
What is the problem? How can I solve that?
UPDATE after the #Emily answer.
If in the above code I use context "foreach user" do ... instead of it "foreach user" do ... I get the following error:
undefined local variable or method `user1' for #<Class:0x00000105310758> (NameError)
The problem is having one spec nested within another. You need to replace it "foreach user" with context "foreach user".
Edited to add: After some investigation, it looks like helpers set with let are only available inside of the it "should ..." block, and not in the surrounding context. I'd recommend is trying to find a different structural solution. What the best solution is will depend on what you're actually trying to test. I'm guessing what you're trying to do is make sure the user is invalid when you remove any of the required attributes. In that case, what I've done is something like this:
describe User do
let(:user_attributes){ Factory.attributes_for(:user) }
# Testing missing values aren't valid
[:name, :email, :phone].each do |required_attribute|
it "should not be valid without #{required_attribute}" do
User.new(user_attributes.except(required_attribute)).should_not be_valid
end
end
# Testing invalid values aren't valid
[[:email, 'not_an_email'], [:phone, 'not a phone']].each do |(attribute, value)|
it "should not be valid with bad value for #{attribute}" do
User.new(user_attributes.update(attribute => value)).should_not be_valid
end
end
end
If you're doing something that requires more complex differences in the instance you're creating, there may not be a clean way to do it with iteration. I don't think DRY is quite as essential in testing as it is in other parts of your code. There's nothing wrong with having three different contexts for the three user types, and a validity test in each context.
describe User do
context "with user1" do
subject{ Factory(:user, :users_attribute_a => 'invalid_value') }
it{ should_not be_valid }
end
context "with user2" do
subject{ Factory(:user, :users_attribute_b => 'invalid_value') }
it{ should_not be_valid }
end
context "with user3" do
subject{ Factory(:user, :users_attribute_c => 'invalid_value') }
it{ should_not be_valid }
end
end
You're mixing and matching all sorts of rspec stuff. Here's your stuff, fixed:
describe User do
let(:user1) { Factory(:user, :users_attribute_a => 'invalid_value') }
let(:user2) { Factory(:user, :users_attribute_b => 'invalid_value') }
let(:user3) { Factory(:user, :users_attribute_c => 'invalid_value') }
it "should not be valid" do
[ user1, user2, user3 ].each do |user|
user.should_not be_valid
end
end
end
I would do it this way:
describe User do
subject{Factory.build(:user)}
it "should not be valid with invalid users_attribute_a" do
subject.users_attribute_a = "invalid_value"
subject.should_not be_valid
end
it "should not be valid with invalid users_attribute_b" do
subject.users_attribute_b = "invalid_value"
subject.should_not be_valid
end
end
If you want to have "context", then cool, but you can't have variables before your context inside of your context.
If you want to have a specification, then have one, but you can't net "it" statements
UPDATE WITH LEAST POSSIBLE CODE
describe User do
it "should not be valid with other attributes" do
{:users_attribute_a => 'invalid_value', :users_attribute_b => 'invalid_value', :users_attribute_c => 'invalid_value'}.each do |key, value|
Factory.build(:user, key => value).should_not be_valid
end
end
end
The problem is that the helpers that are set with "let" do not exist outside of a example context.
What you're trying to do could be achieved as:
it "does something with all users" do
[user1, user2, user3] do |user|
user.valid?.should be_true
end
end
Both contexts are different
Another way it might work (haven't tried it) it's like this:
context "for all users" do
[:user1, :user2, :user3].each do |user|
it "does something" do
send(user).valid?.should be_true
end
end
end
This should work. Note how the context is written, it will make the output of tests clearer. From writing it this way it implies (to me) that you should make a test for each attribute separately, but it's your choice:
describe User do
let!(:users) {
[:users_attribute_a, :users_attribute_b, :users_attribute_c].map do |a|
Factory(:user, => 'invalid_value')
end
}
context "Given a user" do
context "With an invalid value" do
subject { users }
it { subject.all?{|user| should_not be_valid }
end
end
end