rspec refactoring? - ruby-on-rails

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

Related

Testing destroy with RSpec

I have this in controllers
def destroy
#post = Post.find(params[:id])
#post.destroy
end
But I'm lost as to how to actually test if it works. Any pointers would be highly appreciated! I currently have this in my RSpec file:
require 'rails_helper'
RSpec.describe Post, type: :model do
it "must have a title" do
post= Post.create
expect(post.errors[:title]).to_not be_empty
end
it "must have a description" do
post= Post.create
expect(post.errors[:description]).to_not be_empty
end
it "must have a location" do
post= Post.create
expect(post.errors[:location]).to_not be_empty
end
it "must have an image" do
post= Post.create
expect(post.errors[:image]).to_not be_empty
end
it "can be destroyed" do
post= Post.destroy
end
end
You can check if the count of thing has change by -1, like this:
expect { delete '/things', :thing => { :id => 123'} }.to change(Thing, :count).by(-1)
This means that you want to have one less 'thing' and and is ensuring that something has been deleted.
If you want to ensure that specific "thing" was deleted, you can create one before the test, pass the "thing" id as param, and ensure that this doesn't exists on database, like this:
thing = create(:thing)
delete '/things', :thing => { :id => thing.id'}
expect(Thing.find_by(id: thing.id)).to be_nil
As pointed out, if you use request specs ( see https://relishapp.com/rspec/rspec-rails/v/3-9/docs/request-specs/request-spec ) you can easily call the API that should delete the model, and then do an ActiveRecord query to expect no results.
require "rails_helper"
RSpec.describe "delete thing api" do
it "deletes thing" do
// Create a thing with a factory of your choice here
delete "/things", :thing => {:id => 1}
expect(Thing.all.count).to be 0
end
end

Rspec not changing count on create

I am trying to resolve an issue with my rspec test to create an object but the count doesn't seem to change whatever i try. I am sure i am missing something very basic here.
Here is my rspec:
before do
login_account_admin(user)
#group = Factory(:group, :code => "GR_111", :description => "description for GR_111")
Group.stub!(:find).and_return(#group)
end
describe "#create" do
it "should create a new group object" do
group_params = {:code => "NEW_GROUP", :description => "description for NEW_GROUP"}
expect {
post :create, :service_id => service, :cdb_group => group_params, :button => "save", :format => "js"
}.to change(Group, :count).by(1)
end
it "should not create a new group object with invalid code format" do
group_params = {:code => "invalid", :description => "description for invalid code name group"}
expect {
post :create, :service_id => service, :cdb_group => group_params, :button => "save", :format => "js"
}.to_not change(Group, :count)
end
end
"code" parameter can only contain uppercase letters A to Z, 0-9 and _
Here is the controller method definition for #create
def create
#group = Group.new(params[:cdb_group])
respond_to do |format|
if params[:button] == "cancel"
format.js { render "hide_new"}
elsif #group.save
format.js {
render 'show_new_group'
}
format.html { redirect_to(some_path(#service), :notice => 'Group was successfully created.') }
format.xml { head :ok }
end
end
end
Here is the Group model:
class Group < ActiveRecord::Base
validates_uniqueness_of :code
validates_presence_of :code, :description
validates_format_of :code, :without => /[^A-Z0-9_]/ , :message => 'can only contain uppercase letters A to Z, 0-9 and _'
end
Whenever i try to run the rspec test I get the following errors:-
1) GroupsController User As Account Admin goes to #create should create a new group object
Failure/Error: expect {
count should have been changed by 1, but was changed by 0
# ./spec/controllers/groups_controller_spec.rb:51
2) GroupsController User As Account Admin goes to #create should not create a new group object with invalid code format
Failure/Error: expect {
count should not have changed, but did change from 2 to 1
# ./spec/controllers/groups_controller_spec.rb:58
Any help in this regard would be highly appreciated?
Whenever our tests give us unexpected trouble, it's important to take a step back and re-evaluate our approach. Usually, this is an indication of some design problem, either with the code we're testing or with tests themselves.
While it sounds like using a truncation strategy has fixed this particular problem (see more on that below), i would suggest that there is more to learn from the situation.
Consider the two examples from your spec above. The only difference between them comes down to whether the code parameter is valid or not. I would argue that these examples are really testing the Group model, not the controller.
Now, if we're confident in our model test coverage, then we can take a different approach to the controller spec. From the controller's perspective, the model is a collaborator and in general, we always want to avoid indirectly testing collaborators. In this case, we can use a mock to simulate the behavior of the Group model and only test the controller behavior in isolation.
Something like this (please note the code below is incomplete and untested):
# spec/controllers/groups_controller_spec.rb
describe "#create" do
before do
# use a Test Double instead of a real model
#new_group = double(Group)
#params = { :cdb_group => 'stub_cdb_group_param', :service_id => service }
# using should_receive ensures the controller calls new correctly
Group.should_receive(:new).with(#params[:cdb_group]).and_return(#new_group)
end
context "when cancelled responding to js" do
it "renders hide_new" do
post :create, #params.merge({:button => "cancel", :format => "js"})
expect(response).to render_template('hide_new')
end
end
context "with valid params" do
before do
#new_group.should_receive(:save).and_return(true)
end
context "responding to json" # ...
context "responding to html" # ...
context "responding to xml" #...
end
context "with invalid params" do
before do
#new_group.should_receive(:save).and_return(false)
end
# ...
end
end
While the above doesn't specifically address the problem with record counts you were having, i suspect the problem may go away once you isolate your test targets correctly.
If you choose to stick with database truncation, consider using it selectively as described here.
I hope at least some of that helps :).
After fiddling with my spec_helper.rb file. It turns out that i have to change my database cleaning strategy to truncation. Here is my spec_helper file, for reference (https://gist.github.com/aliibrahim/7152042)
I changed this line in my code and disable use of transactional_fixtures
config.use_transactional_fixtures = false
and my database cleaning strategy is now:
config.before(:suite) do
DatabaseCleaner.strategy = :truncation
DatabaseCleaner.clean_with(:truncation)
end
This gives a clear database before the start/end of every scenario. Hope this helps anyone!
You should test...
1) Group.create(group_params).should be_true after group_params = ...
If this fails, the problem probably related to model or test environment.
2) response.status.should == 302 after post ...
If this fails, the problem probably related to session (authentication / authorization).
3) assigns(:group).should be_valid after post ...
If this fails, the problem probably related to controller.

Rails/Rspec: Having tough time writing tests

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

Object.save failed in spec data validation

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.

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