I have a mailer that passes an argument like so:
AnimalMailer.daily_message(owner).deliver_later
The method looks like this:
AnimalMailer
class AnimalMailer < ApplicationMailer
def daily_message(owner)
mail(
to: "#{user.name}",
subject: "test",
content_type: "text/html",
date: Time.now.in_time_zone("Mountain Time (US & Canada)")
)
end
end
I'm new to writing specs and was wondering how should I pass the owner to the method and test it. I currently have this set up:
require "rails_helper"
RSpec.describe AnimalMailer, type: :mailer do
describe "monthly_animal_message" do
let(:user) { create(:user, :admin) }
it "renders the headers" do
expect(mail.subject).to eq("test")
expect(mail.to).to eq(user.name)
end
end
end
Specs generally follow a three-step flow 1) set up, 2) invoke, 3) expect. This applies for unit testing mailers like anything else. The invocation and parameters are the same in the test as for general use, so in your case:
RSpec.describe AnimalMailer, type: :mailer do
describe "monthly_campaign_report" do
let(:user) { create(:user, :admin) }
let(:mail) { described_class.daily_message(user) } # invocation
it 'renders the headers' do
expect(mail.subject).to eq('test')
expect(mail.to).to eq(user.name)
end
it 'renders the body' do
# whatever
end
end
end
Note that since the describe is the class name being tested, you can use described_class from there to refer back to the described class. You can always use AnimalMailer.daily_message as well, but among other things described_class ensures that if you shuffle or share examples that you are always testing what you think you are.
Also note that in the case of unit testing a mailer, you're mostly focused on the correct generation of the content. Testing of successful delivery or use in jobs, controllers, etc., would be done as part of request or feature tests.
Before testing it, make sure the config / environment / test.rb file is set to:
config.action_mailer.delivery_method = :test
This ensures that emails are not actually sent, but are stored in the ActionMailer :: Base.deliveries array.
Following Four-Phase Test :
animal_mailer.rb
class AnimalMailer < ApplicationMailer
default from: 'noreply#animal_mailer.com'
def daily_message(owner)
#name = owner.name
mail(
to: owner.email,
subject: "test",
content_type: "text/html",
date: Time.now.in_time_zone("Mountain Time (US & Canada)")
)
end
end
animal_mailer_spec.rb
RSpec.describe AnimalMailer, type: :mailer do
describe 'instructions' do
let(:user) { create(:user, :admin) }
let(:mail) { described_class.daily_message(user).deliver_now }
it 'renders the subject' do
expect(mail.subject).to eq("test")
end
it 'renders the receiver email' do
expect(mail.to).to eq([user.email])
end
it 'renders the sender email' do
expect(mail.from).to eq(['noreply#animal_mailer.com'])
end
it 'assigns #name' do
expect(mail.body.encoded).to match(user.name)
end
end
end
if you have a model user:
class User
def send_instructions
AnimalMailer.instructions(self).deliver_now
end
end
RSpec.describe User, type: :model do
subject { create :user }
it 'sends an email' do
expect { subject.send_instructions }
.to change { ActionMailer::Base.deliveries.count }.by(1)
end
end
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
I can't find a way around this.
This is my test:
require 'rails_helper'
RSpec.describe V1::UsersController do
describe '#create' do
let(:post_params) do
{
first_nm: Faker::Name.first_name,
last_nm: Faker::Name.last_name ,
password: "test123456",
password_confirmation: "test123456",
email_address: Faker::Internet.email
}
end
before do
post :create, params: post_params
end
context 'successful create' do
subject(:user) { User.find_by_email(post_params[:email_address]) }
it 'persists the user' do
expect(user).not_to be_nil
end
it 'user data is correct' do
post_params.except(:password, :password_confirmation).each do |k, v|
expect(user.send(k)).to eq(v)
end
end
it 'returns responsde code of 201' do
expect(response.status).to eq(201)
end
end
end
end
I only want this controller to be hit once. However, I can't seem to get that to work.
I have tried setting before(:context) and I get an error
RuntimeError:
let declaration `post_params` accessed in a `before(:context)` hook at:
`let` and `subject` declarations are not intended to be called
in a `before(:context)` hook, as they exist to define state that
is reset between each example, while `before(:context)` exists to
define state that is shared across examples in an example group.
I don't want multiple users to be persisted for such a simple test. I also dont want to be hitting the api for every example.
I want the before block to run once. How can I do this?
As the error message states, let and subject are specifically for managing per-example state. But before(:context)/before(:all) hooks get run outside the scope of any specific example, so they are fundamentally incompatible. If you want to use before(:context), you can't reference any let definitions from the hook. You'll have to manage the post_params state yourself without using let. Here's a simple way to do that:
require 'rails_helper'
RSpec.describe V1::UsersController do
describe '#create' do
before(:context) do
#post_params = {
first_nm: Faker::Name.first_name,
last_nm: Faker::Name.last_name ,
password: "test123456",
password_confirmation: "test123456",
email_address: Faker::Internet.email
}
post :create, params: #post_params
end
context 'successful create' do
subject(:user) { User.find_by_email(#post_params[:email_address]) }
it 'persists the user' do
expect(user).not_to be_nil
end
it 'user data is correct' do
#post_params.except(:password, :password_confirmation).each do |k, v|
expect(user.send(k)).to eq(v)
end
end
it 'returns responsde code of 201' do
expect(response.status).to eq(201)
end
end
end
end
That should solve your problem; however it's not the approach I would recommend. Instead, I recommend you use the aggregate_failures feature of RSpec 3.3+ and put all of this in a single example, like so:
require 'rails_helper'
RSpec.describe V1::UsersController do
describe '#create' do
let(:post_params) do
{
first_nm: Faker::Name.first_name,
last_nm: Faker::Name.last_name ,
password: "test123456",
password_confirmation: "test123456",
email_address: Faker::Internet.email
}
end
it 'successfully creates a user with the requested params', :aggregate_failures do
post :create, params: post_params
expect(response.status).to eq(201)
user = User.find_by_email(post_params[:email_address])
expect(user).not_to be_nil
post_params.except(:password, :password_confirmation).each do |k, v|
expect(user.send(k)).to eq(v)
end
end
end
end
aggregate_failures gives you a failure report indicating each expectation that failed (rather than just the first one like normal), just like if you had separated it into 3 separate examples, while allowing you to actually make it a single example. This allows you to incapsulate the action you are testing in a single example, allowing you to only perform the action once like you want. In a lot of ways, this fits better with the per-example state sandboxing provided by RSpec's features like before hooks, let declarations and the DB-transaction rollback provided by rspec-rails, anyway. And
I like the aggregate_failures feature so much that I tend to configure RSpec to automatically apply it to every example in spec_helper.rb:
RSpec.configure do |c|
c.define_derived_metadata do |meta|
meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
end
end
What you are looking for is before(:all), which will run once before all the cases.
There is similar after(:all) as well.
Interestingly, the before is basically a shorter way of saying before(:each) (which IMO makes more sense).
Is it possible to do something like this?
module MyHelper
before (:each) do
allow(Class).to receive(:method).and_return(true)
end
end
Then in my tests I could do something like:
RSpec.describe 'My cool test' do
include MyHelper
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
EDIT: This produces the following error:
undefined method `before' for MyHelper:Module (NoMethodError)
Essentially I have a case where many tests do different things, but a common model across off of them reacts on an after_commit which ends up always calling a method which talks to an API. I dont want to GLOBALLY allow Class to receive :method as, sometimes, I need to define it myself for special cases... but I'd like to not have to repeat my allow/receive/and_return and instead wrap it in a common helper...
You can create a hook that is triggered via metadata, for example :type => :api:
RSpec.configure do |c|
c.before(:each, :type => :api) do
allow(Class).to receive(:method).and_return(true)
end
end
And in your spec:
RSpec.describe 'My cool test', :type => :api do
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
You can also pass :type => :api to individual it blocks.
It is possible to do things like you want with feature called shared_context
You could create the shared file with code like this
shared_file.rb
shared_context "stubbing :method on Class" do
before { allow(Class).to receive(:method).and_return(true) }
end
Then you could include that context in the files you needed in the blocks you wanted like so
your_spec_file.rb
require 'rails_helper'
require 'shared_file'
RSpec.describe 'My cool test' do
include_context "stubbing :method on Class"
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
And it will be more naturally for RSpec than your included/extended module helpers. It would be "RSpec way" let's say.
You could separate that code into shared_context and include it into example groups (not examples) like this:
RSpec.describe 'My cool test' do
shared_context 'class stub' do
before (:each) do
allow(Class).to receive(:method).and_return(true)
end
end
describe "here I am using it" do
include_context 'class stub'
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
describe "here I am not" do
it 'Tests a Class Method' do
expect { Class.method }.not_to eq true
end
end
end
Shared context can contain let, helper functions & everything you need except examples.
https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-context
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