Rails/Rspec: Having tough time writing tests - ruby-on-rails

First, I am a newb when it comes to writing tests in Rails. Thank you [upfront] for your patience.
Here is my class:
require 'json'
class Webhook
attr_accessor :customer_id, :response, :event_type
ACCEPTED_EVENTS = ["customer.subscription.deleted", "invoice.payment_succeeded", "invoice.payment_failed"]
def initialize(json = nil)
if json
#response = JSON.parse(json, symbolize_names: true)
#customer_id = #response[:data][:object][:customer]
#event_type = #response[:type]
#user = User.find_by_customer_id(#customer_id)
end
end
def event_accepted?
true if ACCEPTED_EVENTS.include?(#event_type)
end
def process
return unless event_accepted?
case #event_type
when "invoice.payment_succeeded"
begin
invoice = Stripe::Invoice.retrieve(#response[:data][:object][:id])
InvoiceMailer.payment_succeeded_email(#user, invoice).deliver if invoice.amount_due > 0
rescue => e
Rails.logger.info "An error as occurred! #{e}"
end
when "customer.subscription.deleted"
#user.expire! if #user
when "invoice.payment_failed"
InvoiceMailer.payment_failed_email(#user).deliver
end
end
end
Here is my test thus far:
require 'spec_helper'
describe Webhook do
describe "instance methods" do
let(:webhook) { Webhook.new }
describe "#event_accepted?" do
it "returns true with a correct event_type" do
webhook.event_type = "customer.subscription.deleted"
webhook.event_accepted?.should be_true
end
it "returns false with an incorrect event_type" do
webhook.event_type = "foobar123"
webhook.event_accepted?.should be_false
end
end
end
end
I am a little lost when it comes to trying to write tests for the #process method. Any help would greatly be appreciated!

You have 7 different paths to test for your process method. I am writing the test for two scenarios and leaving the rest for you to try. Also please note that my tests are under the assumption that the other methods process calls are tested separately.
There could be minor syntax/errors here because its untested. But it will give you an idea of how to test the process method
describe "Process" do
it "should do nothing if the event is not accepted" do
webhook = Webhook.new
webhook.stub(:event_accepted?).and_return(false)
InvoiceMailer.should_not_receive(:payment_succeeded_email)
InvoiceMailer.should_not_receive(:payment_failed_email)
webhook.process
end
it "should send a payment succeeded email if the event type is success" do
customer = FactoryGirl.create(:user)
webhook = Webhook.new({"type": "invoice.payment_succeeded", "data": {"object": {"id": 1, "customer": customer.id}}})
Stripe::Invoic.should_receive(:retrieve).with("1").and_return(invoice = double("invoice", :amount_due => 20))
InvoiceMailer.should_receive(:payment_succeeded_email).with(customer, invoice)
webhook.process
end
it "should do nothing if the event type is success but the invoice due is zero" do
end
it "should log when there is an exception in processing the successful payment" do
end
it "should expire the user if the subscription is deleted" do
customer = FactoryGirl.create(:user)
webhook = Webhook.new({"type": "customer.subscription.deleted", "data": {"object": {"id": 1, "customer": customer.id}}})
User.stub(:find_by_customer_id).with(customer.id).and_return(customer)
customer.should_receive(:expire!)
webhook.process
end
it "should do nothing if the subscription is deleted and the user is invalid" do
webhook = Webhook.new({"type": "customer.subscription.deleted", "data": {"object": {"id": 1, "customer": customer.id}}})
User.stub(:find_by_customer_id).with(customer.id).and_return(nil)
User.any_instance.should_not_receive(:expire!)
webhook.process
end
it "should send a failure email if the payment was not successful" do
end
end

Related

Rspec before validation issues

Im trying to write some tests to check before validation of a model in rspec
class MyClass < ApplicationRecord
before_validation :generate_anonymous_id
def generate_anonymous_id
retries ||= 1
self.uuid = SecureRandom.uuid
self.aid = Digest::SHA256.hexdigest(uuid)[0...15]
raise ActiveRecord::RecordNotUnique if OtherClass.find_by_sfid(aid) ||
MyClass.find_by_aid(aid)
rescue ActiveRecord::RecordNotUnique => error
Rails.logger.warn("Encountered duplicate uuid/aid")
retries += 1
retry if retries <= 3
Rails.logger.warn("Raising exception after 3 retries")
raise error
end
end
here is my rspec
# frozen_string_literal: true
require "rails_helper"
RSpec.describe MyClass, type: :model do
describe "model validation" do
let(:my_class) do
MyClass.create
end
context "valid" do
it "allows to create a user aid" do
expect(MyClass.new.valid?).to be_truthy
end
end
context "duplicate" do
subject do
MyClass.new
end
it "allows to create a user aid" do
subject.uuid = my_class.aid
expect(subject.valid?).to be_falsey
end
end
end
end
Im trying to test the rescue block and my test always passes. I'm not able to override my subject and i dont know what is the mistake im doing.
Thanks in advance
Your before_validation overwrites the subject.uuid, which is happening when you call subject.valid? i.e. generate_anonymous_id forces it to be valid
I stubbed the code which raises the exception and it worked.
context "invalid record" do
context "capture exception" do
let!(:user) { create(:custom_user) }
before { allow(UserUuid).to receive(:find_by_sfid).and_return(true)}
subject { described_class.new}
it "raises exception" do
expect { subject.valid? }.to raise_error(ActiveRecord::RecordNotUnique)
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

Rspec method is being called 2X, but can't find second time

Here is my controller spec
before do
#order = Order.new
end
it "should call find & assign_attributes & test delivery_start methods" do
Order.should_receive(:find).with("1").and_return(#order)
Order.any_instance.should_receive(:assign_attributes).with({"id"=>"1", "cancel_reason" => "random"}).and_return(#order)
Order.any_instance.should_receive(:delivery_start).and_return(Time.now)
post :cancel, order: {id:1, cancel_reason:"random"}
end
The failure is this:
Failure/Error: Unable to find matching line from backtrace
(#<Order:0x007fdcb03836e8>).delivery_start(any args)
expected: 1 time with any arguments
received: 2 times with any arguments
# this backtrace line is ignored
But I'm not sure why delivery_start is being called twice based on this controller action:
def cancel
#order = Order.find(cancel_params[:id])
#order.assign_attributes(cancel_params)
if (#order.delivery_start - Time.now) > 24.hours
if refund
#order.save
flash[:success] = "Your order has been successfully cancelled & refunded"
redirect_to root_path
else
flash[:danger] = "Sorry we could not process your cancellation, please try again"
render nothing: true
end
else
#order.save
flash[:success] = "Your order has been successfully cancelled"
redirect_to root_path
end
end
I would suggest you test the behavior and not the implementation. While there are cases where you would want to stub out the database doing it in a controller spec is not a great idea since you are testing the integration between your controllers and the model layer.
In addition your test is only really testing how your controller does its job - not that its actually being done.
describe SomeController, type: :controller do
let(:order){ Order.create } # use let not ivars.
describe '#cancel' do
let(:valid_params) do
{ order: {id: '123', cancel_reason: "random"} }
end
context 'when refundable' do
before { post :cancel, params }
it 'cancels the order' do
expect(order.reload.cancel_reason).to eq "random"
# although you should have a model method so you can do this:
# expect(order.cancelled?).to be_truthy
end
it 'redirects and notifies the user' do
expect(response).to redirect_to root_path
expect(flash[:success]).to eq 'Your order has been successfully cancelled & refunded'
end
end
end
end
I would suggest more expectations and returning true or false depending on your use. Consider the following changes
class SomeController < ApplicationController
def cancel
...
if refundable?
...
end
end
private
def refundable?
(#order.delivery_start - Time.now) > 24.hours
end
end
# spec/controllers/some_controller_spec.rb
describe SomeController, type: :controller do
describe '#cancel' do
context 'when refundable' do
it 'cancels and refunds order' do
order = double(:order)
params = order: {id: '123', cancel_reason: "random"}
expect(Order).to receive(:find).with('123').and_return(order)
expect(order).to receive(:assign_attributes).with(params[:order]).and_return(order)
expect(controller).to receive(:refundable?).and_return(true)
expect(controller).to receive(:refund).and_return(true)
expect(order).to receive(:save).and_return(true)
post :cancel, params
expect(response).to redirect_to '/your_root_path'
expect(session[:flash]['flashes']).to eq({'success'=>'Your order has been successfully cancelled & refunded'})
expect(assigns(:order)).to eq order
end
end
end
end
Sorry, this is a very unsatisfactory answer, but I restarted my computer and the spec passed...
One thing that has been a nuisance for me before is that I've forgotten to save the code, i.e., the old version of the code the test is running against called delivery_start twice. But in this case, I definitely checked that I had saved. I have no idea why a restart fixed it...

rspec problem with mocking some objects

I'm seeing some strange behavior when I'm trying to stub out some methods. I'm using rails 3.0.3 and rspec 2.3.0
Here is the relevant section of the spec file
require 'spec_helper'
include Authlogic::TestCase
describe PrizesController do
before do
activate_authlogic
#manager = Factory.create(:valid_manager, :name => "Test Manager ")
UserSession.create #manager
end
def mock_prize(stubs={})
(#mock_prize ||= mock_model(Prize, :point_cost => 100).as_null_object).tap do |prize|
prize.stub(stubs) unless stubs.empty?
end
end
def mock_consumable(stubs={})
(#mock_consumable ||= mock_model(Consumable).as_null_object).tap do |consumable|
consumable.stub(stubs) unless stubs.empty?
end
end
describe "GET buy_this" do
it "assigns the requested prize as #prize and requested consumable as #consumable if the player has enough points" do
Prize.stub(:find).with("37") { mock_prize }
#manager.should_receive(:available_points).and_return(1000)
get :buy_this, :id => "37", :user_id => #manager.id
assigns(:prize).point_cost.should eq(100)
assigns(:prize).should be(mock_prize)
assigns(:consumable).should_not be_nil
end
it "assigns the requested prize as #prize and no consumable as #consumable if the player does not have enough points" do
Prize.stub(:find).with("37") { mock_prize }
#manager.should_receive(:available_points).and_return(10)
get :buy_this, :id => "37", :user_id => #manager.id
assigns(:prize).point_cost.should eq(100)
assigns(:prize).should be(mock_prize)
assigns(:consumable).should be_nil
end
end
And the controller method:
def buy_this
#prize = Prize.find(params[:id])
user = User.find(params[:user_id]) if params[:user_id]
user ||= current_user
flash[:notice] = ("Attempting to redeem points for a prize")
if user.available_points > #prize.point_cost
#consumable = user.consumables.create(:kind => #prize.consumable_kind, :description => #prize.consumable_description, :redemption_message => #prize.consumable_redemption_message)
point_record = #consumable.create_point_record(:redeemed_points => #prize.point_cost)
point_record.user = user
point_record.save
flash[:success] = "You successfully redeemed #{#prize.point_cost} points for #{#prize.name}"
else
flash[:error] = "Sorry, you don't seem to have enough points to buy this"
end
redirect_to prizes_path
end
The tests fail and this is the output...
1) PrizesController GET buy_this assigns the requested prize as #prize and requested consumable as #consumable if the player has enough points
Failure/Error: assigns(:consumable).should_not be_nil
expected not nil, got nil
# ./spec/controllers/prizes_controller_spec.rb:39
2) PrizesController GET buy_this assigns the requested prize as #prize and no consumable as #consumable if the player does not have enough points
Failure/Error: #manager.should_receive(:available_points).and_return(10)
(#<User:0x10706b000>).available_points(any args)
expected: 1 time
received: 0 times
# ./spec/controllers/prizes_controller_spec.rb:44
Any ideas about this? I'm totally stumped why the two tests calling the same method with the same parameters would fail in different ways (not to mention, I don't understand why they are failing at all...).

rspec refactoring?

I have the following test, with two almost identical blocks. Now i am looking for ways to refactor this cleanly.
The test:
context "with the d1 configuration" do
before (:each) do
# send a message
#envelope = Factory(:envelope, :destination => '32495xxxxxx', :message => 'Message sent by d1')
#distributor = Distributor.find_by_name(Distributor::D1)
#result = #envelope.send_to(#distributor)
end
it "should created a new sms-message" do
#envelope.sent_messages.size.should == 1
end
it "should have created one sms-message linked to the envelope and distributor" do
sms = #envelope.sent_messages.find_by_distributor_id(#distributor.id)
sms.should be_instance_of(SentMessage)
sms.external_message_id.should_not == nil
sms.sent_message_status_id.should == SentMessageStatus::IN_PROGRESS
end
it "should add a logline for the creation of the sms-message" do
#envelope.log_lines.size.should == 2
#envelope.log_lines.last.message.should =~ /^Sent message/
end
end
context "with the correct d2 configuration" do
before (:each) do
# send a message
#envelope = Factory(:envelope, :destination => '32495xxxxxx', :message => 'Message sent by d2')
#distributor = Distributor.find_by_name(Distributor::D2)
#result = #envelope.send_to(#distributor)
end
it "should created a new sms-message" do
#envelope.sent_messages.size.should == 1
end
it "should have created one sms-message linked to the envelope and distributor" do
sms = #envelope.sent_messages.find_by_distributor_id(#distributor.id)
sms.should be_instance_of(SentMessage)
sms.external_message_id.should_not == nil
sms.sent_message_status_id.should == SentMessageStatus::IN_PROGRESS
end
it "should add a logline for the creation of the sms-message" do
#envelope.log_lines.size.should == 2
#envelope.log_lines.last.message.should =~ /^Sent message/
end
end
As you can tell, two identical code blocks, each for a different distributor, D1 and D2 (in our project they have more meaningful names :)) -- and now i need to add a third distributor. How do i go about this?
I can loop over an array containing the changing parts (in this case: distributor-name and the message contents). But can i also change the test-name?
What are the best approaches here? Is it possible to make some kind of test-template, where you can fill in certain values and execute that?
I had a pair-programming session with a more experienced collegue of mine, and together we came up with the following solution.
We defined some shared behaviour first:
subject {#envelope}
let(:the_sent_message){ #envelope.sent_messages.find_by_distributor_id(#distributor.id)}
shared_examples_for "a typical sent envelope" do
it{should have(1).sent_messages }
it{should have(2).log_lines }
end
shared_examples_for "a successful delivery" do
it("should have 1 IN_PROGRESS sms-message") { the_sent_message.should be_in_progress }
it "should have 1 sms-message with external ref" do
the_sent_message.external_message_id.should_not == nil
end
it "should log the delivery success" do
#envelope.log_lines.last.message.should =~ /^Sent message/
end
end
shared_examples_for "a failing delivery" do
it("should have 1 FAILED sms-message") { the_sent_message.should be_failed }
it "should have 1 sms-message and no external ref" do
the_sent_message.external_message_id.should == nil
end
it "should log the delivery failure" do
#envelope.log_lines.last.message.should =~ /^Failed to send/
end
end
and then the tests become way more readable!
context "delivered by d1" do
before do
#distributor = Distributor.find_by_name(Distributor::D1)
send_a_test_envelope_to(#distributor)
end
it_should_behave_like "a typical sent envelope"
it_should_behave_like "a successful delivery"
end
context "delivered by d2" do
before do
#distributor = Distributor.find_by_name(Distributor::D2)
send_a_test_envelope_to(#distributor)
end
it_should_behave_like "a typical sent envelope"
it_should_behave_like "a successful delivery"
end
and we also extracted the following method
def send_a_test_envelope_to(distributor)
#envelope = Factory(:envelope, :destination => '32495xxxxxx', :message => "Message sent by #{#distributor.name}")
#envelope.send_to(distributor)
end
Now i could still apply the suggested answer #Taryn proposed, but i am not entirely sure it is really needed anymore.
Yes, you can loop through an array/hash full of examples and yes you can rename contexts based on that but you'll have to be aware of scoping issues - eg a context is a class-level scope, but a test is an instance.
Thus you have to setup these things in instance-variables in the "setup" section of a context.
I've mainly done this stuff with unit:test+shoulda (rather than rspec) so I may have messed up the scoping rules somewhat, but they should be similarish
Note: I haven't tested the code below, so it may be prey to such issues...
# name this better than I have
CONFIGS = {'d1' => {:name => Distributor::D1
:destination => '32495xxxxxx',
:message => 'd1 message'},
'd2' => {:name => Distributor::D2
:destination => '98765xxxxxx',
:message => 'd2 message'}
} # etc
CONFIGS.each do |display_name, dist_hash|
context "with the #{display_name} configuration" do
before (:each) do
# scope the value-hash here to make it available to test-cases
# (you don't have to if you're just using it in the setup section)
#dist_hash = dist_hash
# send a message
#envelope = Factory(:envelope, :destination => #dist_hash[:destination], :message => #dist_hash[:message])
#distributor = Distributor.find_by_name(#dist_hash[:name])
#result = #envelope.send_to(#distributor)
end
it "should created a new sms-message" do
#envelope.sent_messages.size.should == 1
end
it "should have created one sms-message linked to the envelope and distributor" do
sms = #envelope.sent_messages.find_by_distributor_id(#distributor.id)
sms.should be_instance_of(SentMessage)
sms.external_message_id.should_not == nil
sms.sent_message_status_id.should == SentMessageStatus::IN_PROGRESS
end
it "should add a logline for the creation of the sms-message" do
#envelope.log_lines.size.should == 2
#envelope.log_lines.last.message.should =~ /^Sent message/
end
end
end

Resources