Run before block once with RSpec 3 - ruby-on-rails

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).

Related

Best way to test a mailer with arguments

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

Why is RSpec not passing my headers to Rails?

I am trying to test my Rails application using RSpec, but my tests are failing because RSpec seems to not be passing the headers I give it to Rails.
I have a UsersController that includes ApplicationHelper, and in ApplicationHelper I have a method that accesses the headers hash. Indexing it by my SESSION_KEY header returns nil. If I puts headers inside that method, the hash does not contain the header I have supplied, only the following: {"X-Frame-Options"=>"SAMEORIGIN", "X-XSS-Protection"=>"1; mode=block", "X-Content-Type-Options"=>"nosniff"}.
Here is the relevant part of my RSpec spec:
require 'rails_helper'
describe Api::V1::UsersController, type: :request do
let(:user) { User.create(name: 'TestUser', email: 'someone#example.com', password: 'password123', password_confirmation: 'password123') }
let(:id) { user.id }
let(:sess) { user.sessions.create }
before { get "/api/v1/users/#{id}" }
# Snipped other tests
context 'with authentication' do
context 'with a valid id' do
it 'returns full user information' do
get "/api/v1/users/#{id}", nil, {'HTTP_SESSION_KEY': sess.key}
response_user = response_json[:user]
expect(response.status).to eq 200
expect(response_user).to_not be_nil
expect(response_user[:name]).to eq user[:name]
expect(response_user[:email]).to eq user[:email]
end
end
end
def response_json
JSON.parse(response.body, symbolize_names: true)
end
end
I have also tried passing the SESSION_KEY header without HTTP_ before it, and that did not work. I have also tried moving it up to the top get in the before block to see if it was a context issue, and that did not work either.
Docs say the above should work, but if for some reason rspec is interpreting your test as a :controller test and not a :request test then you need to do this (just before your get call):
request.env["HTTP_SESSION_KEY"] = sess.key

`method_missing': `build` is not available on an example group (e.g. a `describe` or `context` block)

I wanted to remove the FactoryGirl.build(:user) everytime I want to create a user and so I added these lines:
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
to the spec_helper.rb. But this creates the following error:
`method_missing': `build` is not available on an example group (e.g. a `describe` or `context` block). It is only available from within individual examples (e.g. `it` blocks) or from constructs that run in the scope of an example (e.g. `before`, `let`, etc). (RSpec::Core::ExampleGroup::WrongScopeError)
Then I removed all the context/describe blocks, but that didn't change anything. Have any of you had the same problem with this and how may I fix it?
Currently my tests look like so:
require 'rails_helper'
RSpec.describe User, type: :model do
user = build(:user)
project = build(:project)
it "is valid with a firstname, lastname, email and password" do
expect(user).to be_valid
end
it "is invalid without a firstname" do
user = build(:user, name: nil)
expect(user.valid?).to be_falsey
expect(user.errors[:name].size).to eq(1)
end
it "is invalid without a lastname" do
user = build(:user, surname: nil)
expect(user.valid?).to be_falsey
expect(user.errors[:surname].size).to eq(1)
end
it "destroys dependent projects" do
user = User.create!(name: 'john', surname: 'doe', email: 't#example.com', password: 'password', password_confirmation: 'password')
user.projects << project
expect{user.destroy}.to change {Project.count}.by(-1)
end
end
Instead of:
user = build(:user)
project = build(:project)
Do:
let(:user) { build(:user) }
let(:project) { build(:project) }
In general it is not a good idea to define external variables to use them in a test, as this might make your tests order-dependent and extremely hard to debug. Always use the let syntax, so the values are reinitialized for every test.

Mocking and stubbing in testing

I've recently learned how to stub in rspec and found that some benefits of it are we can decouple the code (eg. controller and model), more efficient test execution (eg. stubbing database call).
However I figured that if we stub, the code can be tightly tied to a particular implementation which therefore sacrifice the way we refactor the code later.
Example:
UsersController
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
User.create(name: params[:name])
end
end
Controller spec
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect(User).to receive(:create)
post :create, :name => "abc"
end
end
end
By doing that didn't I just limit the implementation to only using User.create? So later if I change the code my test will fail even though the purpose of both code is the same which is to save the new user to database
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new
#user.name = params[:name]
#user.save!
end
end
Whereas if I test the controller without stubbing, I can create a real record and later check against the record in the database. As long as the controller is able to save the user Like so
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
post :create, :name => "abc"
user = User.first
expect(user.name).to eql("abc")
end
end
end
Really sorry if the codes don't look right or have errors, I didn't check the code but you get my point.
So my question is, can we mock/stub without having to be tied to a particular implementation? If so, would you please throw me an example in rspec
You should use mocking and stubbing to simulate services external to the code, which it uses, but you are not interested in them running in your test.
For example, say your code is using the twitter gem:
status = client.status(my_client)
In your test, you don't really want your code to go to twitter API and get your bogus client's status! Instead you stub that method:
expect(client).to receive(:status).with(my_client).and_return("this is my status!")
Now you can safely check your code, with deterministic, short running results!
This is one use case where stubs and mocks are useful, there are more. Of course, like any other tool, they may be abused, and cause pain later on.
Internally create calls save and new
def create(attributes = nil, options = {}, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, options, &block) }
else
object = new(attributes, options, &block)
object.save
object
end
end
So possibly your second test would cover both cases.
It is not straight forward to write tests which are implementation independent. That's why integration tests have a lot of value and are better suited than unit tests for testing the behavior of the application.
In the code you're presented, you're not exactly mocking or stubbing. Let's take a look at the first spec:
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect(User).to receive(:create)
post :create, :name => "abc"
end
end
end
Here, you're testing that User received the 'create' message. You're right that there's something wrong with this test because it's going to break if you change the implementation of the controllers 'create' action, which defeats the purpose of testing. Tests should be flexible to change and not a hinderance.
What you want to do is not test implementation, but side effects. What is the controller 'create' action supposed to do? It's supposed to create a user. Here's how I would test it
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it 'saves new user' do
expect { post :create, name: 'abc' }.to change(User, :count).by(1)
end
end
end
As for mocking and stubbing, I try to stay away from too much stubbing. I think it's super useful when you're trying to test conditionals. Here's an example:
# /app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
flash[:success] = 'User created'
redirect_to root_path
else
flash[:error] = 'Something went wrong'
render 'new'
end
end
# /spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, :type => :controller do
describe "POST 'create'" do
it "renders new if didn't save" do
User.any_instance.stub(:save).and_return(false)
post :create, name: 'abc'
expect(response).to render_template('new')
end
end
end
Here I'm stubbing out 'save' and returning 'false' so I can test what's supposed to happen if the user fails to save.
Also, the other answers were correct in saying that you want to stub out external services so you don't call on their API every time you're running your test suite.

Testing authentication with Sorcery and RSpec

I've spent far too long messing with this before asking for help. I can't seem to get RSpec and Sorcery to play together nicely. I've read through the docs on Integration testing with Sorcery and can post the login action properly, but my tests still doesn't think the user is logged in.
# spec/controllers/user_controller_spec
describe 'user access' do
let (:user) { create(:user) }
before :each do
login_user(user[:email], user[:password])
end
it "should log in the user" do
controller.should be_logged_in
end
end
And my login_user method
# spec/support/sorcery_login
module Sorcery
module TestHelpers
module Rails
def login_user email, password
page.driver.post(sessions_path, { email: email , password: password, remember_me: false })
end
end
end
end
The sessions controller handles the pages properly when I use them on the generated pages just fine. I tried outputting the results of the login_user method and it appears to properly post the data. How do I persist this logged in user through the tests? Does a before :each block not work for this? I'm just not sure where it could be running wrong and I'm pretty new to testing/RSpec so I may be missing something obvious. I'd appreciate any help.
Here's the output of the failed tests:
1) UsersController user access should log in the user
Failure/Error: controller.should be_logged_in
expected logged_in? to return true, got false
I just went through this yesterday. Here's what I did, if it helps.
Sorcery provides a test helper login_user that relies on a #controller object being available. This works great in controller specs, but doesn't work in integration tests. So the workaround in integration tests is to write another method (like the one you have above) to simulate actually logging in via an HTTP request (essentially simulating submitting a form).
So my first thought is that you should try renaming your method to login_user_post or something else that doesn't collide with the built-in test helper.
Another potential gotcha is that it looks to me like the Sorcery helper assumes that your user's password is 'secret'.
Here's a link to the built-in helper so you can see what I'm talking about:
https://github.com/NoamB/sorcery/blob/master/lib/sorcery/test_helpers/rails.rb
Good luck - I really like this gem except for this part. It is really only fully explained by patching together SO posts. Here's the code I use:
Integration Helper
module Sorcery
module TestHelpers
module Rails
def login_user_post(user, password)
page.driver.post(sessions_url, { username: user, password: password})
end
def logout_user_get
page.driver.get(logout_url)
end
end
end
end
Integration Spec (where user needs to be logged in to do stuff)
before(:each) do
#user = create(:user)
login_user_post(#user.username, 'secret')
end
Controller Spec (where the regular login_user helper works fine)
before(:each) do
#user = create(:user)
login_user
end
Note that login_user doesn't need any arguments if you have an #user object with the password 'secret'.
Did you try adding to spec/spec_helpers.
RSpec.configure do |config|
# ...
config.include Sorcery::TestHelpers::Rails::Controller
end
Nota that you need to include Sorcery::TestHelpers::Rails::Controller, not just Sorcery::TestHelpers::Rails.
Then you will be able to login_user from any controller specs like:
describe CategoriesController do
before do
#user = FactoryGirl::create(:user)
end
describe "GET 'index'" do
it "returns http success" do
login_user
get 'index'
expect(response).to be_success
end
end
end
The way you pass a password is probably wrong. It may be encrypted at this point. In provided example I will try to do this at first:
describe 'user access' do
let (:user) { create(:user, password: 'secret') }
before :each do
login_user(user[:email], 'secret')
end
it "should log in the user" do
controller.should be_logged_in
end
end
This seems to be very poorly documented. The above solutions did not work for me. Here's how I got it to work:
Check your sessions_url. Make sure it is correct. Also, check what params are necessary to log in. It may be email, username, etc.
module Sorcery
module TestHelpers
module Rails
def login_user_post(email, password)
page.driver.post(sessions_url, { email:email, password: password })
end
end
end
end
RSpec config:
config.include Sorcery::TestHelpers::Rails
Spec helper:
def app
Capybara.app
end
spec/controllers/protected_resource_spec.rb:
describe UsersController do
before do
# Create user
# Login
response = login_user_post( user.email, :admin_password )
expect( response.headers[ 'location' ]).to eq 'http://test.host/'
# I test for login success here. Failure redirects to /sign_in.
#cookie = response.headers[ 'Set-Cookie' ]
end
specify 'Gets protected resource' do
get protected_resource, {}, { cookie:#cookie }
expect( last_response.status ).to eq 200
end

Resources