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
Related
I have an AccountsController and a destroy action. I want to test if the account is being deleted and if the subscription is being canceled.
AccountsController
def destroy
Current.account.subscription&.cancel_now!
Current.account.destroy
end
RSpec
describe "#destroy" do
let(:account) { create(:account) }
it "deletes the account and cancels the subscription" do
allow(account).to receive(:subscription)
expect do
delete accounts_path
end.to change(Account, :count).by(-1)
expect(account.subscription).to have_received(:cancel_now!)
end
end
But the above test does not pass. It says,
(nil).cancel_now!
expected: 1 time with any arguments
received: 0 times with any arguments
Because the account.subscription returns nil it is showing this. How do I fix this test?
You need to replace the account entity in the context of the controller to the account from the test
It could be
describe "#destroy" do
let(:account) { create(:account) }
it "deletes the account and cancels the subscription" do
allow(Current).to receive(:account).and_return(account)
# if the subscription does not exist in the context of the account
# then you should stub or create it...
expect do
delete accounts_path
end.to change(Account, :count).by(-1)
expect(account.subscription).to have_received(:cancel_now!)
end
end
Regarding subscription
expect(account).to receive(:subscription).and_return(instance_double(Subscription))
# or
receive(:subscription).and_return(double('some subscription'))
# or
create(:subscription, account: account)
# or
account.subscription = create(:subscription)
# or other options ...
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
I have a method in my controller, unreserve, that calls the find_ticket method and, if the ticket is found, calls unreserve on the ticket.
def unreserve
if find_ticket
#ticket.unreserve current_or_guest_user
render json: #ticket.show.tickets_as_json(current_or_guest_user)
else
render json: nil
end
end
private
def find_ticket
#ticket = Ticket.find params[:id]
rescue ActiveRecord::RecordNotFound
false
else
true
end
Since I have already tested unreserve, I just want to make sure that method is called. So when I try running this spec:
describe 'unreserving a ticket' do
before :each do
sign_in customer
put :reserve, id: ticket_1.id
put :unreserve, id: ticket_1.id
end
it 'calls unreserve on the ticket with the current user' do
expect(controller.instance_variable_get :#ticket).to receive(:unreserve)
end
end
Which I would expect to pass, I even threw a puts statement inside the unreserve method to make sure that method was being called, and it outputted to the console when I was running the test.
The output I get from the test is:
1) TicketsController unreserving a ticket calls unreserve on the ticket with the current user
Failure/Error: expect(controller.instance_variable_get :#ticket).to receive(:unreserve)
(#<Ticket:0x007f90dd6c9600>).unreserve(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
EDIT:
I tried calling the expect statement before the actions, but still got the same error. Here is the new test:
it 'calls unreserve on the ticket with the current user' do
expect(controller.instance_variable_get :#ticket).to receive(:unreserve)
sign_in customer
put :reserve, id: ticket_1.id
put :unreserve, id: ticket_1.id
end
Unless I'm remembering incorrectly you need to make your expectations before you make your request. Meaning you can't use a before block (well not entirely). Move your put :unreserve down after your expect call like so:
describe 'unreserving a ticket' do
before :each do
sign_in customer
put :reserve, id: ticket_1.id
end
it 'calls unreserve on the ticket with the current user' do
expect(controller.instance_variable_get :#ticket).to receive(:unreserve)
put :unreserve, id: ticket_1.id
end
end
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
I have this User class in Ruby on Rails:
class User < ActiveRecord::Base
after_destroy :ensure_an_admin_remains
private
def ensure_an_admin_remains
if User.where("admin = ?", true).count.zero?
raise "Can't delete Admin."
end
end
end
This works great and causes a database rollback if someone accidentally deletes an admin user.
The problem is that it seems to break the user delete action, even when testing with a non-admin user (generated by Factory Girl). This is my user_controller_spec.rb:
describe 'DELETE #destroy' do
before :each do
#user = create(:non_admin_user)
sign_in(#user)
end
it "deletes the user" do
expect{
delete :destroy, id: #user
}.to change(User, :count).by(-1)
end
end
Whenever I run this test, I get this error:
Failure/Error: expect{
count should have been changed by -1, but was changed by 0
There shouldn't be any error, though, because #user's admin attribute is set to false by default.
Can anybody help me out here?
Thanks...
I may be wrong but,
Your spec start with empty database right? So there is no admin user present in your db.
So when you call delete, you'll always have User.where("admin = ?", true).count equal to zero
Try creating an user admin before your test
describe 'DELETE #destroy' do
before :each do
create(:admin_user)
#user = create(:non_admin_user)
sign_in(#user)
end
it "deletes the user" do
expect{
delete :destroy, id: #user
}.to change(User, :count).by(-1)
end
end
I would make the following change:
before_destroy :ensure_an_admin_remains
def ensure_an_admin_remains
if self.admin == true and User.where( :admin => true ).count.zero?
raise "Can't delete Admin."
end
end
An alternative is to make the called function ensure_an_admin_remains a public function, such as check_admin_remains.
You can then test, the logic of check_admin_remains as if it were any other function.
Then in another test, you can ensure that function is called on destroy without any database interaction as follows:
let(:user) { build_stubbed(:user) }
it 'is called on destroy' do
expect(user).to receive(:check_admin_remains)
user.run_callbacks(:destroy)
end
You shouldn't raise for control flow. You can halt during callbacks to prevent the record being commited.
I've improved one some of the answers here for anyone else trying to work out how to do this properly as of Rails 5
class User < ActiveRecord::Base
before_destroy :ensure_an_admin_remains
private def ensure_an_admin_remains
return unless admin && User.where(admin: true).limit(2).size == 1
errors.add(:base, "You cannot delete the last admin.")
throw :abort
end
end