Rspec: object isn't receiving method call from class method test - ruby-on-rails

My test "should call #logout_all" is failing with
expected: 1 time with any arguments
received: 0 times with any arguments
but when I call User.verify_from_token directly in the rails console, I can see that #logout_all is being called (I added a puts statement to #logout_all)
RSpec.describe User, type: :model do
describe ".verify_from_token" do
let(:user) {FactoryGirl.create(:user, verified: false)}
it "should return the user if found" do
token = user.to_valid_token
expect(User.verify_from_token token).to eq(user)
end
it "should verify the user" do
token = user.to_valid_token
User.verify_from_token token
expect(user.reload.verified).to eq(true)
end
it "should call #logout_all" do
token = user.to_valid_token
expect(user).to receive(:logout_all)
User.verify_from_token token
end
end
end
class User < ApplicationRecord
...
def self.verify_from_token token
user = from_token token
if user
user.update_attribute(:verified, true)
user.logout_all
user
else
nil
end
end
...
def logout_all
update_attribute(:token_timestamp, Time.now)
end
end
If I rework the test slightly, it works fine.
it "should call #logout_all" do
token = user.to_valid_token
t1 = user.token_timestamp
User.verify_from_token token
expect(t1 < user.reload.token_timestamp).to eq(true)
end

The problem is that you set expectation on one user, but method call happens on another user. This one.
user = from_token token
You didn't show implementation of from_token, but I'm willing to bet that it loads user from database. Now, there may be only one user in the database and, logically, these two variables represent the same entity. But physically, they're still two different objects in memory. So, naturally, the expectation is not met.

You have to do this:
it "should call #logout_all" do
token = user.to_valid_token
allow(User).to receive(:from_token).once.with(token).and_return(user)
expect(user).to receive(:logout_all)
User.verify_from_token token
end
Like it was said before, the method is called on a new User instance, not the one you set te expectation. You have to stub the "from_token" class method to return the same object you are expecting messages on, not another instance with the same id from the database.

Related

Using RSpec to test a Retry RestClient

I'm using Oauth so what I do is store access_token and refresh token at User table, I create some classes to do this. In the Create class I do the normal functionality of the code (create records on the integration). The access_token expire at 1 hour, so intead of schedule an active job to refresh that token at that time I decided to do Refresh.new(user).call to request a new access_token and refresh_token.
I know that code works, because I've tested on live and I'm getting the new token when the access_token is expired. But I want to do a rspec test for this.
part of my rspec test:.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized).once
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
This is the response:
(RestClient).post(# data)
expected: 1 time with any arguments
received: 2 times with arguments: (# data)
This is my code:
Create.rb
class Create
def initialize(user)
#user = user
#refresh_token = user&.refresh_token
#access_token = user&.access_token
#logger = Rails.logger
#message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
create_contact
rescue RestClient::Unauthorized => ex
retry if Refresh.new(user).call
rescue RestClient::ExceptionWithResponse => ex
logger.error(#message.api_error(ex))
raise
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def create_contact
response = RestClient.post(
url, contact_params, contact_headers
)
logger.info(#message.api_response(response))
end
end
Refresh.rb
class Refresh
def initialize(user)
#user = user
#refresh_token = user&.refresh_token
#access_token = user&.access_token
#logger = Rails.logger
#message = Crm::Message.new(self.class, 'User', user&.id)
end
def call
# validations
refresh_authorization_code
end
def refresh_authorization_code
response = RestClient.post(url, authorization_params)
logger.info(#message.api_response(response))
handle_response(response)
end
private
attr_reader :user, :logger, :access_token, :refresh_token
def handle_response(response)
parsed = JSON.parse(response)
user.update!(access_token: parsed[:access_token], refresh_token: parsed[:refresh_token])
end
end
Also I tried using something like this from here
errors_to_raise = 2
allow(RestClient).to receive(:get) do
return rest_response if errors_to_raise <= 0
errors_to_raise -= 1
raise RestClient::Unauthorized
end
# ...
expect(client_response.code).to eq(200)
but I don't know how handle it propertly.
Your test calls RestClient.post twice, first in Create then again in Retry. But you only mocked one call. You need to mock both calls. The first call raises an exception, the second responds with a successful result.
We could do this by specifying an order with ordered...
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.and_raise(RestClient::Unauthorized)
.ordered
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.and_return(auth_response)
.ordered
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
However, this makes a lot of assumptions about exactly how the code works, and that nothing else calls RestClient.post.
More robust would be to use with to specify responses with specific arguments, and also verify the correct arguments are being passed.
context 'when token is expired' do
it 'request a refresh token and retry' do
old_key = user.access_token
# First call fails
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_raise(RestClient::Unauthorized)
# Second call succeeds and returns an auth response.
# You need to write up that auth_response.
# Alternatively you can .and_call_original but you probably
# don't want your tests making actual API calls.
allow(RestClient)
.to receive(:post)
.with(...whatever the arguments are...)
.and_return(auth_response)
expect { Create.new.call }.to change { user.reload.access_token }.from(old_key)
end
end
But this still makes a lot of assumptions about exactly how the code works, and you need to make a proper response.
Better would be to focus in on exactly what you're testing: when the create call gets an unauthorized exception it tries to refresh and does the call again. This unit test doesn't have to also test that Refresh#call works, just that Create#call calls it. You don't need to have RestClient.post raise an exception, just that Create#create_contact does.
context 'when token is expired' do
it 'requests a refresh token and retry' do
old_key = user.access_token
create = Create.new(user)
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It refreshes
refresh = double
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
# The refresh succeeds
expect(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
# It tries again
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
And you can also test when the retry fails. These can be combined together.
context 'when token is expired' do
let(:refresh) { double }
let(:create) { Create.new(user) }
before {
# First call fails
allow(create)
.to receive(:create_contact)
.and_raise(RestClient::Unauthorized)
.ordered
# It tries to refresh
expect(Refresh)
.to receive(:new)
.with(user)
.and_return(refresh)
}
context 'when the refresh succeeds' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(true)
}
it 'retries' do
expect(create)
.to receive(:create_contact)
.ordered
create.call
end
end
context 'when the refresh fails' do
before {
# The refresh succeeds
allow(refresh)
.to receive(:call)
.with(no_args)
.and_return(false)
}
it 'does not retry' do
expect(create)
.not_to receive(:create_contact)
.ordered
create.call
end
end
end

Rspec: How to test a method that raises an error

I have a SubscriptionHandler class with a call method that creates a pending subscription, attempts to bill the user and then error out if the billing fails. The pending subscription is created regardless of whether or not the billing fails
class SubscriptionHandler
def initialize(customer, stripe_token)
#customer = customer
#stripe_token = stripe_token
end
def call
create_pending_subscription
attempt_charge!
upgrade_subscription
end
private
attr_reader :stripe_token, :customer
def create_pending_subscription
#subscription = Subscription.create(pending: true, customer_id: customer.id)
end
def attempt_charge!
StripeCharger.new(stripe_token).charge! #raises FailedPaymentError
end
def upgrade_subscription
#subscription.update(pending: true)
end
end
Here is what my specs look like:
describe SubscriptionHandler do
describe "#call" do
it "creates a pending subscription" do
customer = create(:customer)
token = "token-xxx"
charger = StripeCharger.new(token)
allow(StripeCharger).to receive(:new).and_return(charger)
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
handler = SubscriptionHandler.new(customer, token)
expect { handler.call }.to change { Subscription.count }.by(1) # Fails with FailedPaymentError
end
end
end
But this does not change the subscription count, it fails with the FailedPaymentError. Is there a way to check that the subscription count increases without the spec blowing up with FailedPaymentError.
You should be able to use Rspec compound expectations for this
https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations
So I'll re-write your expectation to something like this:
expect { handler.call }.
to raise_error(FailedPaymentError).
and change { Subscription.count }.by(1)
It can be done like this
expect{ handler.call }.to raise_error FailedPaymentError
Should work.
If you don't want to raise error at all then you can remove this line, and return a valid response instead
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
More info - How to test exception raising in Rails/RSpec?
Official RSpec docs
https://relishapp.com/rspec/rspec-expectations/v/2-0/docs/matchers/expect-error

Testing a signup confirmation with Rspec/Factory Girl/Rails

Trying to create an Rspec/Factory girl test to make sure that Devise's confirmation on signup is covered - the site has 3 languages (Japanese, English, Chinese) so I want to make sure nothing breaks the signup process.
I have the following factories:
user.rb << Has everything needed for the general user mailer tests
signup.rb which has:
FactoryGirl.define do
factory :signup do
token "fwoefurklj102939"
email "abcd#ek12o9d.com"
end
end
The devise user_mailer method that I want to test is:
def confirmation_instructions(user, token, opts={})
#user = user
set_language_user_only
mail to: #user.email,
charset: (#user.language == User::LANGUAGE_JA ? 'ISO-2022-JP' : 'UTF8')
end
I cannot for the life of me figure out how to get the token part to work in the test - any advice or ideas?
I have been trying something along these lines (to check the email is being sent) without success:
describe UserMailer, type: :mailer do
describe "sending an email" do
after(:all) { ActionMailer::Base.deliveries.clear }
context "Japanese user emails" do
subject(:signup) { create(:signup) }
subject(:user) { create(:user) }
subject(:mail) do
UserMailer.confirmation_instructions(user, token, opts={})
end
it "sends an email successfully" do
expect { mail.deliver }.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
end
end
The resulting error is undefined local variable or methodtoken'and I cannot work out why it is not coming from thesignup` factory. I tried changing
subject(:mail) do
UserMailer.confirmation_instructions(user, token, opts={})
end
to
subject(:mail) do
UserMailer.confirmation_instructions(user, signup.token, opts={})
end
but then I received this error:
Failure/Error: subject(:signup) { create(:signup) }
NameError:
uninitialized constant Signup
EDIT: I forgot to mention something important - the actual code all works for user signups in all 3 languages, so I am certain that this is definitely my inexperience with testing at fault.
subject(:mail) do
UserMailer.confirmation_instructions(user, user.confirmation_token)
end
This varies of course depending on what your exact implementation is but your user class should be generating the token:
require 'secure_random'
class User
before_create :generate_confirmation_token!
def generate_confirmation_token!
confirmation_token = SecureRandom.urlsafe_base64
end
end
Creating a separate factory is unnecessary and won't work since FactoryGirl will try to create an instance of Signup which I'm guessing that you don't have.
Factories are not fixtures.

Mocking the model and its methods

Can anyone tell me whether my approach is fine?
I want to know the api we are using is working as designed and when it changes so we know that has changed without having to dig through all the code logic. We will provide a set of arguments, we expect a certain result so that my unit tests work well.
User.login({email: username, password: password);
The above method in my model actually hits the API and returns me a response. I want to check whether my Model's login method work as intended.
Below is my approach.
I am stubbing my login method in my model with required params and expected response, to avoid hitting the api and then expecting the login method to derive the same response.
I am using ActiveRestClient.
Below is my model
class User << ActiveRestClient::Base
get :all, '/user'
get :find, '/user/:id'
end
Below is my spec
require 'spec_helper'
describe User do
let(:username) {"test#test.com"}
let(:password) {"123"}
context "when signing in" do
let(:response) {{token: "123"}.to_json}
it "should sign in with valid input" do
allow(User).to receive(:login).with({email: username, password:
password}).and_return(response)
expect(User.login({email: username, password: passwor})).to eq(response)
end
end
end
Can anyone tell me whether my approach is fine?
No, I am sorry, your approach is not fine. Because it does not test a single line of our code. The only thing your spec is testing is that the User.login stub returns what you told it to return.
If you want to speed up your specs by stubbing methods, than you should look for calls in your method that touch the database. Something like User.find_by_email in the following example (And I guess you do something similar in your login method).
Furthermore you may want to spec want happens if the email or the password does not match.
describe User do
describe 'login' do
let(:username) { "test#test.com" }
let(:password) { "password" }
subject(:login) { User.login(email: username, password: password) }
context 'when user do not exists' do
before { allow(User).to receive(:find_by_email).and_return(nil) }
it 'returns nil' do
expect(login).to be_nil
end
end
context 'when user exists' do
before do
allow(User).to receive(:find_by_email).with(username).and_return(user)
end
context 'when password does not match' do
let(:user) { User.new(:password => 'wrong password') }
it 'returns nil' do
expect(login).to be_nil
end
end
context 'when password matches' do
let(:user) { User.new(:password => password, :generate_token => 123) }
it 'returns a json containing the signin token' do
expect(login).to eq "{'token':'123'}"
end
end
end
end
end
Since I have no idea what your login method really does, all specs above are just based on assumptions and will very like not pass with your implementation. But I hope you get the point.

Devise.friendly_token returns an equal hash all the time

I am all mixed up trying to understand what happens here:
I use Devise.friendly_token in a User factory.
FactoryGirl.define do
factory :user do
email "user#example.com"
password "secret"
authentication_token Devise.friendly_token
end
end
In some tests I use the factory as follows:
require 'spec_helper'
describe SessionsController do
before do
#user = User.gen!
puts "Token = #{#user.authentication_token}" # <--- debugging output
end
describe "#create" do
context "when sending ..." do
it "renders a json hash ..." do
api_sign_in #user
expect(last_response.status).to eq(201)
end
end
context "when sending ..." do
it "renders a json hash ..." do
user = User.gen!(email: "invalid#email.com")
puts "Token2 = #{user.authentication_token}" # <--- debugging output
api_sign_in user
expect(last_response.status).to eq(422)
end
end
end
describe "#destroy" do
context "when sending ..." do
it "renders a json hash ..." do
api_sign_out #user
expect(last_response.status).to eq(200)
end
end
end
end
The debugging output shows that the token is the same hash on every call.
Strange! When I test Devise.friendly_token in the console it generates a random hash on every execution. That's what I expect looking at the implementation.
I guess there is a major design problem ... Please help me out.
This line:
authentication_token Devise.friendly_token
will call Devise.friendly_token only once when the factory is initialized. You want
authentication_token { Devise.friendly_token }
which will evaluate the block every time an object is created by FactoryGirl.

Resources