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
Related
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
I'm trying to split my code in RSpec into multiple files so it looks nicer. The current file looks like this.
require 'rails_helper'
RSpec.describe Api::MyController do
let(:var1) {}
let(:var2) {}
it 'should calculate some value' do
expect(var1 + var2).to eq('some value')
end
end
Now this is how it looks after refactoring.
require 'rails_helper'
require_relative './mycontroller/calculation'
RSpec.describe Api::MyController do
let(:var1) {}
let(:var2) {}
include Api::MyController::Calculation
end
And this is how calculation.rb looks like.
module Api::MyController::Calculation
it 'should calculate some value' do
expect(var1 + var2).to eq('some value')
end
end
The problem now is that when it runs, it complains var1 and var2 is not defined.
I believe you are looking for RSpec's shared examples:
# spec/support/shared_examples/a_calculator.rb
RSpec.shared_examples "a calculator" do
it 'should calculate some value' do
expect(x+y).to eq(result)
end
end
You then include the shared example with any of:
include_examples "name" # include the examples in the current context
it_behaves_like "name" # include the examples in a nested context
it_should_behave_like "name" # include the examples in a nested context
matching metadata # include the examples in the current context
You can pass context to the shared example by passing a block:
require 'rails_helper'
require 'support/shared_examples/a_calculator'
RSpec.describe Api::MyController do
it_should_behave_like "a calculator" do
let(:x){ 1 }
let(:y){ 2 }
let(:result){ 3 }
end
end
I have a helper method that uses 'request' to determine the URL. However, rspec can't seem to find request. I thought request was available to all front-facing tests?
How can I account for the request method in my spec?
Helper Spec
require 'spec_helper'
describe ApplicationHelper do
describe "full_title" do
it "should include the page title" do
expect(full_title("help")).to include('help')
end
end
end
Helper methods
def full_title(page_title)
if staging? # causing the issue
base_title = "Staging"
else
base_title = "Company Name"
end
if page_title.empty?
"#{base_title} | Tag line "
else
"#{base_title} | #{page_title} "
end
end
def staging? # the request here seems to be the problem
request.original_url.include? "staging"
end
Rspec error
Failure/Error: expect(full_title("help")).to include('help')
NameError:
undefined local variable or method `request' for #<RSpec::ExampleGroups::ApplicationHelper_2::FullTitle:0x00000106260078>
Thanks in advance.
First off: request is only available in the controller tests (and even then only in the request specs I think), helper tests are really basic and isolated. Which is good. Your helper code should be really minimal and normally only work on the input it receives.
However this is pretty easily solvable by using stubbing.
So write something like
#note, OP needed to replace 'helper' with 'self'for Rails 4.0.0 and Rspec 3.0
require 'rails_helper'
describe ApplicationHelper do
describe "full_title" do
context "in staging" do
it "should include the page title" do
helper.should_receive(:staging?).and_return(true)
expect(full_title("help")).to include('help')
end
end
context "not in staging" do
it "should include the page title" do
helper.should_receive(:staging?).and_return(false)
expect(full_title("help")).to include('help')
end
end
end
end
Which is imho a very clear, and then you write separate tests for your staging? method:
describe "staging?" do
context "when in staging" do
it "returns true" do
helper.stub(:request) { OpenStruct.new(original_url: 'staging') }
expect( helper.staging? ).to be true
end
end
context "when not in staging" do
it "returns false" do
helper.stub(:request) { OpenStruct.new(original_url: 'development') }
expect(helper.staging?).to be false
end
end
end
end
Some small remarks: ruby default indentation is 2 spaces.
Secondly, your function now literally says return true if true, ideally it should be written like
def staging?
request.original_url.include? "staging"
end
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
I would like write RSpec for my controller using RR.
I wrote following code:
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe RegistrationController do
it "should work" do
#deploy and approve are member functions
stub.instance_of(Registration).approve { true }
stub.instance_of(Registration).deploy { true }
post :register
end
end
However RR stubs only deploy method when still calls original approve method.
What syntax should I use to stub both method calls for all instances of Registration class?
UPDATE:
I achivied desired result with [Mocha]
Registration.any_instance.stubs(:deploy).returns(true)
Registration.any_instance.stubs(:approve).returns(true)
It would appear the behavior you describe is actually a bug:
http://github.com/btakita/rr/issues#issue/17
as far as I know, the RSpec mocks don't allow you to do that. Are you sure, that you need to stub all instances? I usually follow this pattern:
describe RegistrationController do
before(:each) do
#registration = mock_model(Registration, :approve => true, :deploy => true)
Registration.stub!(:find => #registration)
# now each call to Registration.find will return my mocked object
end
it "should work" do
post :register
reponse.should be_success
end
it "should call approve" do
#registration.should_receive(:approve).once.and_return(true)
post :register
end
# etc
end
By stubbing the find method of the Registration class you control, what object gets returned in the spec.