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...).
Related
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...
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
I have a test that creates the following error:
1) Failure:
test_should_get_create(ProductRequestsControllerTest) [/Users/noahc/Dropbox/mavens/test/functional/product_requests_controller_test.rb:37]:
"ProductRequest.count" didn't change by 1.
<2> expected but was
<1>.
How do I trouble shoot this? Specifically, how can I get a more specific detailed error?
Here is my test:
test "should get create" do
sign_in(FactoryGirl.create(:user))
assert_difference('ProductRequest.count') do
post :create, product_request: FactoryGirl.attributes_for(:product_request)
end
assert_response :success
end
and here is my controller:
def create
cart = current_cart
rows = CartRow.find_all_by_cart_id(cart.id)
rows.each do |row|
product_request = ProductRequest.new(params[:product_request])
product_request.user_id = current_user.id
product_request.product_id = row.product_id
product_request.quantity = row.quantity
product_request.save
end
redirect_to root_path
end
I believe the issue is that I don't have a cart defined. How do I create a cart that unit::test can see? I've tried using FactoryGirl to create a cart, but that didn't seem to work.
carts_factory.rb
FactoryGirl.define do
factory :cart do
end
end
Updated test:
test "should get create" do
sign_in(FactoryGirl.create(:user))
user = FactoryGirl.create(:user)
product = FactoryGirl.create(:product)
assert_difference('ProductRequest.count') do
post :create, product_request: FactoryGirl.attributes_for(:product_request, user: user.id, product: product.id)
end
assert_response :success
end
and current_cart
def current_cart
Cart.find(session[:cart_id])
rescue ActiveRecord::RecordNotFound
cart = Cart.create
session[:cart_id] = cart.id
cart
end
Second Update
I've updated the factories as you've suggested.
Here is what my test now looks like:
test "should get create" do
user = FactoryGirl.create(:user)
cart = FactoryGirl.create(:cart_with_1_row)
product = FactoryGirl.create(:product)
sign_in(user)
product = FactoryGirl.create(:product)
assert_difference('ProductRequest.count') do
post :create, { product_request: FactoryGirl.attributes_for(:product_request, user_id: user.id, product_id: product.id, cart_id: cart.id) }
end
assert_response :success
end
Here's it in a test console:
irb(main):016:0> a = { product_request: FactoryGirl.attributes_for(:product_request, user_id: user.id, product_id: product.id, cart_id: cart.id) }
=> {:product_request=>{:quantity=>10, :street=>"123 street", :city=>"Some City", :state=>"Iowa", :zip=>"13829", :user_id=>1, :product_id=>2, :cart_id=>1}}
First of all, CartRow.find_all_by_cart_id(cart.id), this is not good design. Much better when you ask Cart model for its row, for example: rows = cart.rows
I think the issue is that you don't have rows inside you cart.
As I see you store cart id in session but when you call controller in test you does not provide session. You need create cart and cart's rows and when store cart_id in session before you call controller. And it is important to merge current session and session with cart_id. For example:
test "should get create" do
user = FactoryGirl.create(:user)
cart = FactoryGirl.create(:cart_with_1_row)
sign_in(user)
product = FactoryGirl.create(:product)
assert_difference('ProductRequest.count') do
post :create, { product_request: FactoryGirl.attributes_for(:product_request, user: user.id, product: product.id) }, { cart_id: cart.id }.merge(session)
end
assert_response :success
end
Also you need update your cart and cart's row factories:
FactoryGirl.define do
factory :cart do
factory :cart_with_1_row do
after(:create) do |cart|
FactoryGirl.create(:cart_row, cart: cart)
end
end
end
factory :cart_row do
cart
end
end
I think your CartRow model looks like:
class CartRow < ActiveRecord::Base
belongs_to :cart
end
The problem definitely seems to be coming from the cart.
If you don't want to deal with creating a cart in FactoryGirl (which I would recommend), you can just stub out the current_card in the test, and this would do the same thing.
However, mocking is much more complicated than creating the cart in FactoryGirl, and if you're planning on using this down the road, FactoryGirl is definitely the way to go.
Here is the failed spec code for create in customer controller:
describe CustomersController do
before(:each) do
#the following recognizes that there is a before filter without execution of it.
controller.should_receive(:require_signin)
controller.should_receive(:require_employee)
end
render_views
describe "'create' successful" do
before(:each) do
category = Factory(:category)
sales = Factory(:user)
#customer = Factory.attributes_for(:customer, :category1_id => category.id, :sales_id => sales.id)
session[:sales] = true
session[:user_id] = sales.id
session[:user_name] = sales.name
session[:page_step] = 1
session['page1'] = customers_path
end
it "should create one customer record" do
lambda do
post 'create', #customer
end.should change(Customer, :count).by(1)
end
it "should redirect to customers path" do
put 'create', #customer
flash[:notice].should_not be_nil
response.should redirect_to(customers_path)
end
end
end
The customer has both sales id and category id which belong to user and category table respectively.
Here is the spec failure error:
1) CustomersController GET customer page 'create' successful should create one customer record
Failure/Error: lambda do
count should have been changed by 1, but was changed by 0
# ./spec/controllers/customers_controller_spec.rb:37:in `block (4 levels) in <top (required)>'
2) CustomersController GET customer page 'create' successful should redirect to customers path
Failure/Error: flash[:notice].should_not be_nil
expected: not nil
got: nil
# ./spec/controllers/customers_controller_spec.rb:44:in `block (4 levels) in <top (required)>'
Here is the app code for create in customer controller:
def create
if session[:sales]
#customer = Customer.new(params[:customer], :as => :roles_new_update)
#customer.sales_id = session[:user_id]
if #customer.save
#message = "New customer #{params[:name]} was created. Please check it out"
#subject = "New customer #{params[:name]} was created BY {#session[:user_name]}"
UserMailer.notify_tl_dh_ch_ceo(#message, #subject, session[:user_id])
redirect_to session[('page' + session[:page_step].to_s).to_sym], :notice => 'Customer was created successfaully!'
else
render 'new', :notice => 'Customer was not saved!'
end
end
end
Here is the code in factories.rb:
Factory.define :customer do |c|
c.name "test customer"
c.short_name "test"
c.email "t#acom.com"
c.phone "12345678"
c.cell "1234567890"
c.active 1
c.category1_id 2
c.sales_id 1
c.address "1276 S. Highland Ave, Lombard, IL 67034"
c.contact "Jun C"
end
Factory.define :category do |c|
c.name "category name"
c.description "test category"
c.active true
end
Factory.define :user do |user|
user.name "Test User"
user.email "test#test.com"
user.password "password1"
user.password_confirmation "password1"
user.status "active"
user.user_type "employee"
end
It seems that the error was caused by #customer.save returning false and the code for "if #customer.save" was not executed. So the problem may be with the #customer generated by Factory which seems good to me. The code is executed without any problem when saving a customer.
Any suggestions? Thanks.
I would break this up into two specific tests. Right now you're unsure of two things:
is the customer is being told to save itself?
Is there a validation that is preventing customer from being saved?
The quickest path is to change #customer.save to #customer.save! and see if there are any exceptions raised (it will do so if a validation failed).
I recommend you split this up though. To test #1, in the controller spec:
it "should tell the customer to save itself when there is a session[:sales]" do
session[:sales] = true
customer_mock = double(:customer)
customer_mock.should_receive(:sales_id=)
customer_mock.should_receive(:save).and_return(:true)
Customer.stub(:new => cutomer_mock)
post 'create'
end
Then in your customer_spec, test out:
it "should be valid with factory specs" do
customer = Customer.new(Factory.attributes_for(:customer))
customer.should be_valid
end
post :create, :customer => #customer
solves the problem with above.
I was wondering if i could have some feedbacks with the controller spec bellow. In fact i'm new when writing specs and controller's spec are way different from model's spec ! So i'm wondering if i may not go in the wrong direction...
subjects_controller.rb
def show
#subject = Subject.find(params[:id])
if #subject.trusted?(current_user)
#messages = #subject.messages
else
#messages = #subject.messages.public
#messages = #messages + #subject.messages.where(:user_ids => current_user.id)
#messages.uniq!
end
# sort the list
#messages = #messages.sort_by(&:created_at).reverse
if !#subject.company.id == current_user.company.id
redirect_to(subjects_path, :notice => "Invalid subject")
end
end
subjects_controller_spec.rb
require 'spec_helper'
describe SubjectsController do
before(:each) do
#subject = mock_model(Subject)
end
context "for signed users" do
before(:each) do
#current_user = sign_in Factory(:user)
end
context "GET #show" do
before(:each) do
Subject.stub!(:find, #subject).and_return(#subject)
end
context "when current_user is trusted" do
before(:each) do
messages = []
company = mock_model(Company)
#subject.should_receive(:trusted?).and_return(true)
#subject.should_receive(:messages).and_return(messages)
#subject.should_receive(:company).and_return(company)
end
it "should render success" do
get :show, :id => #subject
response.should be_success
end
end
context "when current_user is not trusted" do
before(:each) do
messages = []
company = mock_model(Company)
#subject.should_receive(:trusted?).and_return(false)
#subject.should_receive(:messages).and_return(messages)
messages.should_receive(:public).and_return(messages)
#subject.should_receive(:messages).and_return(messages)
messages.should_receive(:where).and_return(messages)
#subject.should_receive(:company).and_return(company)
end
it "should render success" do
get :show, :id => #subject
response.should be_success
end
end
context "when subject's company is not equal to current_user's company" do
# I have no idea of how to implement ==
end
end
end
end
Factories.rb
Factory.define :user do |u|
u.first_name 'Test User' #
u.username 'Test User' #
u.surname 'TheTest' #
u.email 'foo#foobar.com' #
u.password 'please' #
u.confirmed_at Time.now #
end
As far as I can tell you're on the right path. The basic idea is to completely isolate your controller code from model and view in these tests. You appear to be doing that--stubbing and mocking model interaction.
Don't write RSpec controller specs at all. Use Cucumber stories instead. Much easier, and you get better coverage.