How do I test stripe and my controller effectively? - ruby-on-rails

I have the following models:
subscription, user, and events
A user has_one subscription
A subscription belongs_to a user
A user has_many events
An event belongs_to a user
So far I have been able to successfully create acceptance tests using Capybara and RSpec. This allows me to 'upgrade' a users account (which adds a different role). I've also been able to do an acceptance test where the user cancels their subscription and ensure their roles are removed.
However, now I want to ensure that any of the user's open events are cancelled. This is where I'm getting stuck. Actually, I didn't even get this far because I ran in to trouble trying to even destroy a subscription.
So, I created a controller spec called subscriptions_controller_spec.rb. In this spec, there is a test to ensure the destroy action works as expected. This is failing because in my controller it goes to retrieve the customer and subscription, which doesn't exist, and returns an Stripe::InvalidRequestError.
In order to get around this, I tried to use stripe-ruby-mock to mock the stripe servers. However, I'm not sure how I'm supposed to use this in a controller spec and I got really confused. Below is my controller and my controller spec. Any advice on how I should attack this would be really appreciated.
subscriptions_controller_spec.rb
require 'rails_helper'
RSpec.describe SubscriptionsController, :type => :controller do
let(:stripe_helper) { StripeMock.create_test_helper }
before { StripeMock.start }
after { StripeMock.stop }
# ... omitted
describe 'DELETE destroy' do
before :each do
sign_in_trainer
#subscription = create(:subscription, user: subject.current_user)
plan = stripe_helper.create_plan(:id => 'Standard')
customer = Stripe::Customer.create({
email: 'johnny#appleseed.com',
source: stripe_helper.generate_card_token,
plan: 'Standard'
})
#subscription.customer_id = customer.id
#subscription.stripe_sub_id = customer.subscriptions.data.first.id
end
it 'destroys the requested subscription' do
expect {
delete :destroy, {:id => #subscription.to_param}
}.to change(Subscription, :count).by(-1)
end
# ... omitted
end
end
And subscriptions_controller.rb
class SubscriptionsController < ApplicationController
before_action :set_subscription, only: [:update, :destroy]
# ... ommitted
# DELETE /cancel-subscriptions/1
def destroy
begin
customer = Stripe::Customer.retrieve(#subscription.customer_id)
customer.subscriptions.retrieve(#subscription.stripe_sub_id).delete
rescue Stripe::CardError => e
# User's card was declined for many magnitude of reasons
redirect_to user_dashboard_path, alert: 'There was a problem cancelling your subscription' and return
rescue Stripe::APIConnectionError => e
# Stripe network issues
redirect_to user_dashboard_path, alert: 'Network issue. Please try again later' and return
rescue Stripe::APIError => e
# Stripe network issues
redirect_to user_dashboard_path, alert: 'Network issue. Please try again later' and return
rescue Stripe::InvalidRequestError => e
# This is something that we screwed up in our programming. This should literally never happen.
redirect_to user_dashboard_path, alert: 'There was a problem cancelling your subscription.' and return
rescue => e
logger.error e.message
logger.error e.backtrace.join("\n")
redirect_to user_dashboard_path, alert: 'There was a problem cancelling your subscription.' and return
end
if current_user.events
#events = current_user.events
#events.open.each do |event|
event.cancel
end
end
current_user.remove_role 'trainer'
current_user.add_role 'user'
current_user.save
#subscription.destroy
respond_to do |format|
format.html { redirect_to user_dashboard_path, notice: 'Subscription cancelled. All your open events have been cancelled.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_subscription
#subscription = Subscription.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def subscription_params
params[:subscription]
end
end

I think you've kind of already hit the nail on the head here, the fact that it's hard to test in a controller spec indicates that it might be a good time to consider moving the behaviour to a service class.
What i'd do is setup an integration test to use as your feedback loop, then refactor and get back to green. Once you've done that, start refactoring your service class and building on your specs from there.

Does simply mocking out Stripe not work eg:
require 'rails_helper'
RSpec.describe SubscriptionsController, :type => :controller do
# ... omitted
describe 'DELETE destroy' do
before :each do
sign_in_trainer
#subscription = create(:subscription, user: subject.current_user)
end
it 'destroys the requested subscription' do
# just mock stripe to pass back the customer you expect - as though it Just Works
expect(Stripe::Customer).to receive(:retreive).and_return(subscription.customer)
expect {
delete :destroy, {:id => #subscription.to_param}
}.to change(Subscription, :count).by(-1)
end
it 'does not destroy it if we got a card error' do
# likewise you can mock up what happens when an error is raised
expect(Stripe::Customer).to receive(:retreive).and_raise(Stripe::CardError)
expect {
delete :destroy, {:id => #subscription.to_param}
}.not_to change(Subscription, :count)
end
# ... omitted
end
end

Related

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 controller test for callback after_save

I am trying to test to see if posting to a create method in my controller triggers a callback I defined with after_save
Here's the controller method being posted to
def create
#guest = Guest.new(guest_params)
#hotel = Hotel.find(visit_params[:hotel_id])
#set visit local times to UTC
#visit= Visit.new(visit_params)
#visit.checked_out_at = (DateTime.now.utc + visit_params[:checked_out_at].to_i.to_i.days).change(hour: #visit.hotel.checkout_time.hour)
#visit.checked_in_at = Time.now.utc
##visit.user_id = current_user.id
#self_serve = (params[:self_serve] && params[:self_serve] == "true")
if #guest.save
#visit.guest_id = #guest.id
if #visit.save
if #self_serve
flash[:notice] = "#{#visit.guest.name}, you have successfully checked in!."
redirect_to guest_checkin_hotel_path(#visit.hotel)
else
flash[:notice] = "You have successfully checked in #{#visit.guest.name}."
redirect_to hotel_path(#visit.hotel)
end
else
render "new"
end
else
render "new"
end
end
Here's my spec/controllers/guests_controller_spec.rb test that is failing
RSpec.describe GuestsController, :type => :controller do
describe "#create" do
let!(:params) do { name: "John Smith", mobile_number: "9095551234" } end
context "when new guest is saved" do
it "triggers create_check_in_messages callback" do
post :create, params
expect(response).to receive(:create_check_in_messages)
end
end
end
end
Here is my models/concerns/visit_message.rb callback file
module VisitMessage
extend ActiveSupport::Concern
included do
after_save :create_check_in_messages
end
def create_check_in_messages
. . .
end
end
Here is the fail message when I run 'rspec spec/controllers/guests_controller_spec.rb'
1) GuestsController#create when new guest is saved triggers create_check_in_messages callback
Failure/Error: post :create, params
ActionController::ParameterMissing:
param is missing or the value is empty: guest
# ./app/controllers/guests_controller.rb:63:in `guest_params'
# ./app/controllers/guests_controller.rb:10:in `create'
# ./spec/controllers/guests_controller_spec.rb:36:in `block (4 levels) in <top (required)>'
I've been searching all over stackoverflow with no luck. I appreciate any help!
I am assuming that the guest_params method in the controller looks something like this:
def guest_params
params.require(:guest).permit(....)
end
If that is the case, you need to update the POST call in your test case thusly:
post :create, {guest: params}
On a side note, your controller is unnecessarily bloated. I would read up on working with associated models to streamline your code, specifically, using accepts_nested_attributes_for:
http://guides.rubyonrails.org/association_basics.html#detailed-association-reference
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

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.

Rails/Rspec - testing a redirect in the controller

So I am currently writing a test for a controller in an existing controller that just didn't have one before. What I want to test is a redirect that happens when someone is not allowed to edit something vs someone that is allowed to edit it.
the controller action being edit
def edit
if !#scorecard.reviewed? || admin?
#company = #scorecard.company
#custom_css_include = "confirmation_page"
else
redirect_to :back
end
end
So if a scorecard has been reviewed then only an admin can edit that score.
The routes for that controller..
# scorecards
resources :scorecards do
member do
get 'report'
end
resources :inaccuracy_reports, :only => [:new, :create]
end
and finally the test
require 'spec_helper'
describe ScorecardsController do
describe "GET edit" do
before(:each) do
#agency = Factory(:agency)
#va = Factory(:va_user, :agency => #agency)
#admin = Factory(:admin)
#company = Factory(:company)
#scorecard = Factory(:scorecard, :level => 1, :company => #company, :agency => #agency, :reviewed => true)
request.env["HTTP_REFERER"] = "/scorecard"
end
context "as a admin" do
before(:each) do
controller.stub(:current_user).and_return #admin
end
it "allows you to edit a reviewed scorecard" do
get 'edit', :id => #scorecard.id
response.status.should be(200)
end
end
context "as a va_user" do
before(:each) do
controller.stub(:current_user).and_return #va
end
it "does not allow you to edit a reviewed scorecard" do
get 'edit', :id => #scorecard.id
response.should redirect_to :back
end
end
end
end
so a va when trying to edit a reviewed score will be redirected back, where an admin won't.
but when running this through rspec I get
ScorecardsController
GET edit
as a admin
allows you to edit a reviewed scorecard
as a va_user
does not allow you to edit a reviewed scorecard (FAILED - 1)
Failures:
1) ScorecardsController GET edit as a va_user does not allow you to edit a reviewed scorecard
Failure/Error: response.should redirect_to :back
Expected response to be a redirect to </scorecard> but was a redirect to <http://test.host/>
# ./spec/controllers/scorecards_controller_spec.rb:33:in `block (4 levels) in <top (required)>'
Finished in 0.48517 seconds
2 examples, 1 failure
so I don't know if its working or not since I set the request.env["HTTP_REFERER"] = "/scorecard" as the place that should be the :back as it where. or am I missing the idea all together looking at httpstatus there are the 300 responses that I could use but I wouldn't know where to start?
any help would be awesome
EDIT
I could test it by doing it like this
...
response.status.should be(302)
but I got the idea from this question and it sounds like this could be powerful as it specifies the url redirected to.
Anyone have a working test like this?
To make the test more readable you can do this:
(rspec ~> 3.0)
expect(response).to redirect_to(action_path)
This line has problem
response.should redirect_to :back
The logic is not correct. You should expect #edit to redirect to :back path you set before, which is /scorecard. But you set :back here. In the context of Rspec, :back should be empty at each example.
To revise, just set it as
response.should redirect_to '/scorecard'
For testing if redirects happened, with no matching route
(just to test redirection, i used this when route is too long :D ).
You can simply do like:
expect(response.status).to eq(302) #redirected
In my case, it was not returning a response. If you end up in this situation, you can do:
expect(page.current_path).to eql('expected/path')

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

Resources