I'm testing my Sidekiq job and I'm trying to figure out how to ensure that it called a method within the job with the correct arguments. Here is my actual job:
JOB:
def perform
csv = Campaign.to_csv
email = current_sso_user.email
CampaignMailer.send_csv(email, csv).deliver_now
end
I'd like to build a test that ensures the CampaignMailer.send_csv was called with the correct args.
Here is what I have currently:
TEST:
RSpec.describe CampaignsExortJob, type: :model do
subject(:job) { CampaignsExportJob.new }
describe '#perform' do
let(:campaign) { create(:campaign) }
it 'sends the csv email' do
expect(job).to receive(:CampaignMailer.send_csv)
end
end
end
But this is a syntax error. Can anyone give me some guidance on how to test this properly? Thank You!
From the rspec docs(https://relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/expect-message-using-should-receive):
expect a message with an argument
Given
a file named "spec/account_spec.rb" with:
require "account"
require "spec_helper"
describe Account do
context "when closed" do
it "logs an account closed message" do
logger = double("logger")
account = Account.new logger
logger.should_receive(:account_closed).with(account)
account.close
end
end
end
And
a file named "lib/account.rb" with:
Account = Struct.new(:logger) do
def close
logger.account_closed(self)
end
end
When
I run rspec spec/account_spec.rb
Then
the output should contain "1 example, 0 failures"
so in your case:
CampaignMailer.should_receive(:send_csv).with(<expected_email_arg>, <expected_csv_arg>)
You can also add on a .and_return(<expected_return_value>) to test the return value.
Related
I have a worker that sends email to users when new feedback pops in. I wanted to allow user to not agree to that (with Shih Tzu flags). Question is: how can I test (with Rspec) if the FedbackMailer.new_feedback line gets executed?
account.users.each do |user|
return if (user.no_notifications || user.just_summary)
FeedbackMailer.new_feedback(account.id, feedback_id, user.id).deliver_later
end
You can use rspec-mocks.
mailer = instance_double
allow(FeedbackMailer).to receive(:new_feedback).with(account_id, feedback_id, user_id).and_return(mailer)
allow(mailer).to receive(:deliver_later)
## do stuff ##
expect(mailer).to have_received(:deliver_later)
You can also ignore the .with if you haven't the arguments to pass in that moment.
Another solution is to set a config config.action_mailer.delivery_method = :test and check if the delivery counts have changed.
expect {
## code that deliver the email
}.to change { ActionMailer::Base.deliveries.count }.by(1)
Let's say your logic is encapsulated in following method in class MyClass
class MyClass
def my_method
account.users.each do |user|
return if (user.no_notifications || user.just_summary)
FeedbackMailer.new_feedback(account.id, feedback_id, user.id).deliver_later
end
end
end
RSpec.describe MyClass, type: :model do
context "#my_method" do
it "should send new feedback" do
user_obj = create_user
expect(user_obj.no_notifications).to be_falsey
#OR
#expect(user_obj.just_summary).to be_falsey
account_obj = create_account
account_obj.users << user_obj
expect(account_obj.users).to include(user_obj)
expect(FeedbackMailer).to receive(:new).with(account_obj.id, feedback_id, user_obj.id)
# OR in case you don't have feedback_id then you can use
# expect(FeedbackMailer).to receive(:new).with(account_obj.id, kind_of(Numeric), user_obj.id)
# You should also setup expectation here that `FeedbackMailer` gets enqueued to ensure
# that your method also gets invoked and the job also gets enqueued.
subject.my_method
end
end
end
Hope that helps. Thanks.
I am setting up RSpec request tests, and I have the following test:
require 'rails_helper'
RSpec.describe "ClientApi::V1::ClientContexts", type: :request do
describe "POST /client_api/v1/client_contexts" do
let(:client_context) { build :client_context }
it "creates a new context" do
post "/client_api/v1/client_contexts", params: {
browser_type: client_context.browser_type,
browser_version: client_context.browser_version,
operating_system: client_context.operating_system,
operating_system_version: client_context.operating_system_version
}
expect(response).to have_http_status(200)
expect(json.keys).to contain_exactly("browser_type", "browser_version", "operating_system", "operating_system_version")
# and so on ...
end
end
end
The corresponding factory is this:
FactoryBot.define do
factory :client_context do
browser_type { "Browser type" }
browser_version { "10.12.14-blah" }
operating_system { "Operating system" }
operating_system_version { "14.16.18-random" }
end
end
Now, obviously, that all seems a bit redundant. I have now three places in which I specify the attributes to be sent. If I ever want to add an attribute, I have to do it in all of these places. What I actually want to do is send the particular attributes that the Factory specifies via POST, and then check that they get returned as well.
Is there any way for me to access the attributes (and only these!) that I defined in the Factory, and re-use them throughout the spec?
I should prefix this with a warning that abstracting away the actual parameters from the request being made could be seen as detrimental to the overall test expressiveness. After all, now you'd have to look into the Factory to see which parameters are sent to the server.
You can simply get the Factory-defined attributes with attributes_for:
attributes_for :client_context
If you need more flexibility, you can implement a custom strategy that returns an attribute Hash from your Factory without creating the object, just building it.
Create a file spec/support/attribute_hash_strategy.rb:
class AttributeHashStrategy
def initialize
#strategy = FactoryBot.strategy_by_name(:build).new
end
delegate :association, to: :#strategy
def result(evaluation)
evaluation.hash
end
end
Here, the important part is evaluation.hash, which returns the created object as a Ruby Hash.
Now, in your rails_helper.rb, at the top:
require 'support/attribute_hash_strategy'
And below, in the config block, specify:
# this should already be there:
config.include FactoryBot::Syntax::Methods
# add this:
FactoryBot.register_strategy(:attribute_hash, AttributeHashStrategy)
Now, in the Spec, you can build the Hash like so:
require 'rails_helper'
RSpec.describe "ClientApi::V1::ClientContexts", type: :request do
describe "POST /client_api/v1/client_contexts" do
let(:client_context) { attribute_hash :client_context }
it "creates a new context" do
client = create :client
post "/client_api/v1/client_contexts",
params: client_context
expect(response).to have_http_status(200)
end
end
end
The attribute_hash method will be a simple Hash that you can pass as request parameters.
I am currently trying to test a portion of code in my rails worker class as shown below(simplified version);
class SenderWorker
include Sidekiq::Worker
sidekiq_options :retry => 5
def perform(current_user_guid)
Rails.logger.info "Starting for user_guid: #{current_user_guid}"
user = User.find_by!(guid: current_user_guid)
team = Team.find_by!(uuid: user.team.uuid)
profiles = team.profiles
profiles.each do |profile|
SenderClass.new(profile,
user).send(User::RECALL_USER)
end
Rails.logger.info "Finishing for user_guid: #{current_user_guid}"
end
end
The tests that I have written are these and they are passing;
context 'when something occurs' do
it 'should send' do
sender = double("sender")
allow(SenderClass).to receive(:new).with(user_profile, current_user) { sender }
expect(sender).to receive(:send)
expect(Rails.logger).to receive(:info).exactly(2).times
worker.perform(user.guid)
end
end
However, I am not testing for all calls. Is there a way to ensure that I test for everything called in the each do loop. Thank you in advance.
You can test that :send is received an expected amount of times.
But I'd suggest you to simplify the test by using a class method to encapsulate those chained methods. Something like:
def self.profile_send(profile, user)
new(profile, user).send(User::RECALL_USER)
end
Then:
def perform(current_user_guid)
Rails.logger.info "Starting for user_guid: #{current_user_guid}"
user = User.find_by!(guid: current_user_guid)
team = Team.find_by!(uuid: user.team.uuid)
profiles = team.profiles
profiles.each do |profile|
SenderClass.profile_send(profile, user)
end
Rails.logger.info "Finishing for user_guid: #{current_user_guid}"
end
And now you can test that SenderClass receives :send_profile X times.
You can then add a test for SenderClass.send_profile if you really want to test the new and the send method calls, but then you can test that once, outside a loop and both tests will cover what you want.
I have a Mailer that looks something like this:
class EventMailer < BaseMailer
def event_added(user_id, event_id)
# do stuff and email user about the event
end
end
I'm calling this EventMailer like this from inside the Event class:
class Event < Task
def notify_by_email(user)
EmailLog.send_once(user.id, id) do
EventMailer.delay(queue: 'mailer').event_added(user.id, id)
end
end
end
where EmailLog is a class that logs sent emails. .delay is added by Sidekiq.
But when I try to test that #notify_by_email is called only once per event and user, my spec fails:
1) Event#notify_by_email only sends once per user
Failure/Error: expect(EventMailer).to receive(:event_added).once
(<EventMailer (class)>).event_added(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
The spec looks like:
let(:owner) { User.make! }
let(:product) { Product.make! }
let(:event) { Task.make!(user: owner, product: product) }
describe '#notify_by_email' do
before do
EventMailer.stub(:delay).and_return(EventMailer)
end
it 'only sends once per user' do
event.notify_by_email(owner)
event.notify_by_email(owner)
expect(EventMailer).to receive(:event_added).once
end
end
Any insights into why this spec is failing and how I can fix it? Strangely, if I put a puts statement inside the block that's passed to EmailLog.send_once, it prints only once, the spec still reports that EventMailer.event_added wasn't called.
Your expectation should be declared before the code you're testing. Using expect(...).to receive(...) basically means "this message should be received between now and the end of this spec". Because the expectation is the last line of your spec, it fails.
Try moving it before and you should be good to go:
it 'only sends once per user' do
expect(EventMailer).to receive(:event_added).once
event.notify_by_email(owner)
event.notify_by_email(owner)
end
I have a test that looks like this:
class PageTest < ActiveSupport::TestCase
describe "test" do
test "should not save without attributes" do
page = Page.new
assert !page.save
end
end
end
When running the tests, I get 0 tests, 0 assertions. If I remove the describe "test" do, I get the 1 test, 1 assertions. So I have the feeling that the describe "..." do is actually making the test disappear.
What is going on here? What am I missing?
Looks like you're mixing up minitest specs and ActiveSupport::TestCase. If you check the rails guides on testing the test method is explained but it's not used with describe.
Rails adds a test method that takes a test name and a block. It
generates a normal MiniTest::Unit test with method names prefixed with
test_. So,
test "the truth" do
assert true
end
acts as if you had written
def test_the_truth
assert true
end
The describe syntax is explained in the minitest docs under the spec section and is used with it (and not test). Like so:
describe "when asked about cheeseburgers" do
it "must respond positively" do
#meme.i_can_has_cheezburger?.must_equal "OHAI!"
end
end