How should I spec this - ruby-on-rails

The following spec works but I know it shouldn't be like this. I am having a hard time getting my head around rspec, in particular mocks and stubs.
This is the model code
class RecipeFermentable < ActiveRecord::Base
belongs_to :recipe
belongs_to :product
def set_attributes()
attrs = product.product_attributes
self.ppg = attrs.find_by_name(:ppg.to_s).value
self.ecb = attrs.find_by_name(:ecb.to_s).value
end
end
And this is the spec I have written
it "should set the attributes from the product" do
#product_attribute1 = mock_model(ProductAttribute, :name => :ppg, :value => 40)
#product_attribute2 = mock_model(ProductAttribute, :name => :ecb, :value => 1)
#product = Product.new
#product.product_attributes << #product_attribute1
#product.product_attributes << #product_attribute2
#recipe_fermentable = RecipeFermentable.new
#recipe_fermentable.product.should_receive(:product_attributes).and_return(#product_attributes)
#product_attributes.stub(:find_by_name).with(:ppg.to_s).and_return(#product_attribute1)
#product_attributes.stub(:find_by_name).with(:ecb.to_s).and_return(#product_attribute2)
#recipe_fermentable.set_attributes
#recipe_fermentable.ppg.should eql(40)
#recipe_fermentable.ecb.should eql(1)
end
For a start my spec is way bigger than my method, and I am using a real Product. Some pointers on the way to write a goodbetter spec for this would be really helpfull. Also if anyone knows of a good resource for learning rspec using mocks and stubs, please could you add some links.
Thanks

I would change a couple of things here:
Much of the code in your it is just providing context, so it should be in your before(:each) block.
You are setting a message expectation, but it doesn't really seem like you are testing for that. I think the expectation should be switched to a stub instead. Another test could be it 'should call product_attributes', where you would in fact test that expectation - I am not advocating you do this since you would be testing implementation and not behavior, but just making the point.
You are returning #product_attributes in that message expectation, and using it right after to stub the find_by_name calls. However, you never defined #product_attributes. I assume that should be a mock object, and I'm not sure what it really is in that context. Maybe it is nil, and you are stubbing a couple of methods on it.
With those two changes, here's where we are:
before(:each) do
#product = mock_model(Product)
#product_attribute_ppg = mock_model(ProductAttribute, :name => :ppg, :value => 40)
#product_attribute_ecb = mock_model(ProductAttribute, :name => :ecb, :value => 1)
#product_attributes = mock('product_attributes')
#product_attributes.stub!(:find_by_name).with(:ppg.to_s).and_return(#product_attribute_ppg)
#product_attributes.stub!(:find_by_name).with(:ecb.to_s).and_return(#product_attribute_ecb)
#product.stub!(:product_attributes).and_return(#product_attributes)
#recipe_fermentable = RecipeFermentable.new
#recipe_fermentable.stub!(:product).and_return(#product)
end
it 'should set the attributes from the product' do
#recipe_fermentable.set_attributes
#recipe_fermentable.ppg.should eql(40)
#recipe_fermentable.ecb.should eql(1)
end
With all of that all of the way, I don't completely agree with your approach here. I think that you are repeating data and moving away from DB normalization. Unless there's a real reason for that (could be that your way ahead and for performance reasons you had to do this), I would suggest the following instead:
class RecipeFermentable < ActiveRecord::Base
def ppg
#rescue nil here so that if attributes is nil, or find_by_name('ppg') is nil, things don't blow up
product.attributes.find_by_name('ppg').value rescue nil
end
#other
end
A couple of resources for testing:
RSpec book
xUnit patterns: not RSpec nor Ruby, but a must.

Related

FactoryGirl attribute set in after(:create) doesnt persist until referenced?

Sorry for the vague title, there are a lot of moving parts to this problem so I think it will only be clear after seeing my code. I'm fairly sure I know what's going on here and am looking for feedback on how to do it differently:
I have a User model that sets a uuid attr via an ActiveRecord callback (this is actually in a "SetsUuid" concern, but the effect is this):
class User < ActiveRecord::Base
before_validation :set_uuid, on: :create
validates :uuid, presence: true, uniqueness: true
private
def set_uuid
self.uuid = SecureRandom.uuid
end
end
I am writing a functional rspec controller test for a "foo/add_user" endpoint. The controller code looks like this (there's some other stuff like error-handling and #foo and #params being set by filters, but you get the point. I know this is all working.)
class FoosController < ApplicationController
def add_user
#foo.users << User.find_by_uuid!(#params[:user_id])
render json: {
status: 'awesome controller great job'
}
end
end
I am writing a functional rspec controller test for the case "foo/add_user adds user to foo". My test looks roughly this (again, leaving stuff out here, but the point should be obvious, and I know it's all working as intended. Also, just to preempt the comments: no, I'm not actually 'hardcoding' the "user-uuid" string value in the test, this is just for the example)
RSpec.describe FoosController, type: :controller do
describe '#add_user' do
it_behaves_like 'has #foo' do
it_behaves_like 'has #params', {user_id: 'user-uuid'} do
context 'user with uuid exists' do
let(:user) { create(:user_with_uuid, uuid: params[:user_id]) } # params is set by the 'has #params' shared_context
it 'adds user with uuid to #foo' do
route.call() # route is defined by a previous let that I truncated from this example code
expect(foo.users).to include(user) # foo is set by the 'has #foo' shared_context
end
end
end
end
end
end
And here is my user factory (I've tried setting the uuid in several different ways, but my problem (that I go into below) is always the same. I think this way (with traits) is the most elegant, so that's what I'm putting here):
FactoryGirl.define do
factory :user do
email { |n| "user-#{n}#example.com" }
first_name 'john'
last_name 'naglick'
phone '718-555-1234'
trait :with_uuid do
after(:create) do |user, eval|
user.update!(uuid: eval.uuid)
end
end
factory :user_with_uuid, traits: [:with_uuid]
end
end
Finally, The problem:
This only works if I reference user.uuid before route.call() in the spec.
As in, if I simply add the line "user.uuid" before route.call(), everything works as intended.
If I don't have that line, the spec fails because the user's uuid doesn't actually get updated by the after(:create) callback in the trait in the factory, and thus the User.find_by_uuid! line in the controller does not find the user.
And just to preempt another comment: I'm NOT asking how to re-write this spec so that it works like I want. I already know a myriad of ways to do this (the easiest and most obvious being to manually update user.uuid in the spec itself and forget about setting the uuid in the factory altogether). The thing I'm asking here is why is factorygirl behaving like this?
I know it has something to do with lazy-attributes (obvious by the fact it magically works if I have a line evaluating user.uuid), but why? And, even better: is there some way I can do what I want here (setting the uuid in the factory) and have everything work like I intend? I think it's a rather elegant looking use of rspec/factorygirl, so I'd really like it to work like this.
Thanks for reading my long question! Very much appreciate any insight
Your issue has less to do with FactoryGirl and more to do with let being lazily evaluated.
From the docs:
Use let to define a memoized helper method. The value will be cached across
multiple calls in the same example but not across examples.
Note that let is lazy-evaluated: it is not evaluated until the first time
the method it defines is invoked. You can use let! to force the method's
invocation before each example.
Since your test doesn't invoke the user object until the expectation there is nothing created. To force rspec to load object, you can use let!.
Instead of using the before_validation callback you should be using after_initialize. That way the callback is fired even before .valid? is called in the model lifecycle.
class User < ActiveRecord::Base
before_initialization :set_uuid!, on: :create, if: :set_uuid?
validates :uuid, presence: true, uniqueness: true
private
def set_uuid!
# we should also check that the UUID
# does not actually previously exist in the DB
begin
self.uuid = SecureRandom.uuid
end while User.where(uuid: self.uuid).any?
end
def set_uuid?
self.uuid.nil?
end
end
Although the chance of generating the same hash twice with SecureRandom.uuid is extremely slim it is possible due to the pigeonhole principle. If you maxed out in the bad luck lottery this would simply generate a new UUID.
Since the callback fires before validation occurs the actual logic here should be completely self contained in the model. Therefore there is no need to setup a callback in FactoryGirl.
Instead you would setup your spec like so:
let!(:user) { create(:user) }
it 'adds user with uuid to #foo' do
post :add_user, user_id: user.uuid, { baz: 3 }
end

How to write a Klass.any_instance_with_id(id).expects(:method) in RSpec

When writing tests using RSpec, I regularly have the need to express something like
Klass.any_instance_with_id(id).expects(:method)
Main reason is that in my test, I often have the object that should receive that method call available, but due to the fact that ActiveRecord, when loading the object with that id from the database, will create a different instance, I can't put the "expects" on my own instance
Sometimes I can stub the find method to force ActiveRecord to load my instance, sometimes I can stub other methods, but having that "any_instance_with_id" would make life so much easier...
Can't image I'm the first having this problem... So if any of you found a "workaround", I'd be glad to find out!
Example illustrating the need:
controller spec:
describe 'an authorized email' do
let(:lead) { create(:lead, status: 'approved') }
it "should invoice its organisation in case the organisation exceeds its credit limit" do
lead.organisation.expects :invoice_leads
get :email
end
end
controller:
def email
leads = Lead.approved
leads.each do |lead|
lead.organisation.invoice_leads if lead.organisation.credit_limit_exceeded?
end
redirect_to root_path
end
It seems weird to me you need that for specs.
You should take the problem one level higher: when your app tries to retrieve the record.
Example:
#code
#user = User.find(session[:user_id])
# spec
let(:fake_user) { mock_model 'User', method: false }
it 'description' do
User.should_receive(:find).and_return fake_user
fake_user.expects(:method)
#...
end
Order/invoice example:
let(:order) { mock_model 'order', invoice: invoice }
let(:invoice) { mock_model 'Invoice', 'archive!' => false }

How to write short, clean rspec tests for method with many model calls?

I'm having trouble coming up with some tests for a method I want to write.
The method is going to take a hash of some data and create a bunch of associated models with it. The problem is, I'm having a hard time figuring out what the best practice for writing this sort of test is.
For example, the code will:
Take a hash that looks like:
{
:department => 'CS',
:course_title => 'Algorithms',
:section_number => '01B'
:term => 'Fall 2012',
:instructor => 'Bob Dylan'
}
And save it to the models Department, Course, Section, and Instructor.
This will take many calls to model.find_or_create, etc.
How could I go about testing each separate purpose of this method, e.g.:
it 'should find or create department' do
# << Way too many stubs here for each model and all association calls
dept = mock_model(Department)
Department.should_receive(:find_or_create).with(:name => 'CS').and_return(dept)
end
Is there a way to avoid the massive amounts of stubs to keep each test FIRST (fast independent repeatable self-checking timely) ? Is there a better way to write this method and/or these tests? I'd really prefer to have short, clean it blocks.
Thank you so much for any help.
Edit:
The method will probably look like this:
def handle_course_submission(param_hash)
department = Department.find_or_create(:name => param_hash[:department])
course = Course.find_or_create(:title => param_hash[:course_title])
instructor = Instructor.find_or_create(:name => param_hash[:instructor])
section = Section.find_or_create(:number => param_hash[:section_number], :term => param_hash[:term])
# Maybe put this stuff in a different method?
course.department = department
section.course = course
section.instructor = instructor
end
Is there a better way to write the method? How would I write the tests? Thanks!
For passing an array of sections to be created:
class SectionCreator
# sections is the array of parameters
def initialize(sections)
#sections = sections
end
# Adding the ! here because I think you should use the save! methods
# with exceptions as mentioned in one of my myriad comments.
def create_sections!
#sections.each do |section|
create_section!(section)
end
end
def create_section!(section)
section = find_or_create_section(section[:section_number], section[:term])
section.add_course!(section_params)
end
# The rest of my original example goes here
end
# In your controller or wherever...
def action
SectionCreator.new(params_array).create_sections!
rescue ActiveRecord::RecordInvalid => ex
errors = ex.record.errors
render json: errors
end
Hopefully this covers it all.
My first thought is that you may be suffering from a bigger design flaw. Without seeing the greater context of your method it is hard to give much advice. However, in general it is good to break the method up into smaller pieces and follow the single level of abstraction principle.
http://www.markhneedham.com/blog/2009/06/12/coding-single-level-of-abstraction-principle/
Here is something you could try although as mentioned before this is definitely still not ideal:
def handle_course_submission(param_hash)
department = find_or_create_department(param_hash[:department])
course = find_or_create_course(param_hash[:course_title])
# etc.
# call another method here to perform the actual work
end
private
def find_or_create_department(department)
Department.find_or_create(name: department)
end
def find_or_create_course(course_title)
Course.find_or_create(title: course_title)
end
# Etc.
In the spec...
let(:param_hash) do
{
:department => 'CS',
:course_title => 'Algorithms',
:section_number => '01B'
:term => 'Fall 2012',
:instructor => 'Bob Dylan'
}
end
describe "#save_hash" do
before do
subject.stub(:find_or_create_department).as_null_object
subject.stub(:find_or_create_course).as_null_object
# etc.
end
after do
subject.handle_course_submission(param_hash)
end
it "should save the department" do
subject.should_receive(:find_or_create_department).with(param_hash[:department])
end
it "should save the course title" do
subject.should_receive(:find_or_create_course).with(param_hash[:course_title])
end
# Etc.
end
describe "#find_or_create_department" do
it "should find or create a Department" do
Department.should_receive(:find_or_create).with("Department Name")
subject.find_or_create_department("Department Name")
end
end
# etc. for the rest of the find_or_create methods as well as any other
# methods you add
Hope some of that helped a little. If you post more of your example code I may be able to provide less generalized and possibly useful advice.
Given the new context provided, I would split the functionality up amongst your models a little more. Again, this is really just the first thing that comes to mind and could definitely be improved upon. It seems to me like the Section is the root object here. So you could either add a Section.create_course method or wrap it in a service object like so:
Updated this example to use exceptions
class SectionCreator
def initialize(param_hash)
number = param_hash.delete(:section_number)
term = param_hash.delete(:term)
#section = find_or_create_section(number, term)
#param_hash = param_hash
end
def create!
#section.add_course!(#param_hash)
end
private
def find_or_create_section(number, term)
Section.find_or_create(number: number, term: term)
end
end
class Section < ActiveRecord::Base
# All of your current model stuff here
def add_course!(course_info)
department_name = course_info[:department]
course_title = course_info[:course_title]
instructor_name = param_hash[:instructor]
self.course = find_or_create_course_with_department(course_title, department_name)
self.instructor = find_or_create_instructor(instructor_name)
save!
self
end
def find_or_create_course_with_department(course_title, department_name)
course = find_or_create_course(course_title)
course.department = find_or_create_department(department_name)
course.save!
course
end
def find_or_create_course(course_title)
Course.find_or_create(title: course_title)
end
def find_or_create_department(department_name)
Department.find_or_create(name: department_name)
end
def find_or_create_instructor(instructor_name)
Instructor.find_or_create(name: instructor_name)
end
end
# In your controller (this needs more work but..)
def create_section_action
#section = SectionCreator.new(params).create!
rescue ActiveRecord::RecordInvalid => ex
flash[:alert] = #section.errors
end
Notice how adding the #find_or_create_course_with_department method allowed us to add the association of the department in there while keeping the #add_course method clean. That is why I like to add those methods even though they sometimes seem superflous like in the case of the #find_or_create_instructor method.
The other advantage of breaking out the methods in this fashion is that they become easier to stub in tests as I showed in my first example. You can easily stub all of these methods to make sure the database isn't actually being hit and your tests run fast while at the same time guarantee through the test expectations that the functionality is correct.
Of course, a lot of this comes down to personal preference on how you want to implement it. In this case the service object is probably unnecessary. You could just as easily have implemented that as the Section.create_course method I referenced earlier like so:
class Section < ActiveRecord::Base
def self.create_course(param_hash)
section = find_or_create(number: param_hash.delete(:section_number), term: param_hash.delete(:term))
section.add_course(param_hash)
section
end
# The rest of the model goes here
end
As to your final question, you can definitely stub out methods in RSpec and then apply expectations like should_receive on top of those stubs.
It's getting late so let me know if I missed anything.

Rspec Rails: testing controller method 'create' with a multi-model form

I am building a Ruby on Rails app with the usual assortment of models, views and controllers.
The 'create' action in one of my controllers is supposed to create an instance of two different models. Here's my code:
def create
#league = League.new(params[:league])
#user = #league.users.build(params[:user])
... .save conditions appended ...
end
So, when you call 'create' through the LeaguesController via a POST request to /leagues, you get a new instance of League and a new instance of User. I also want the new User instance to inherit the ID of the new League instance, so it can be used as the foreign key to link the instances together. This is accomplished with:
def create
#league = League.new(params[:league])
#user = #league.users.build(params[:user])
#league_id = #league.id
#user.update_attribute('league_id', #league_id)
... .save conditions appended ...
end
The User model already belongs_to the League model, which has_many users.
The above code works just fine and dandy, verified via manual testing. However, I can't for the life of me figure out how to automate these tests with Rspec. I'm trying to be a good boy and use test-driven design, but this has me stumped.
The issue is that I can't figure out how to access the attributes of the newly created instances of League and User in my tests. I am attempting to do so using the following code:
describe LeaguesController do
describe 'new league and user' do
it 'should create a new user with a league_id equal to the new leagues id'
#league_attr = { :name => "myleague", :confirmation_code => "mycode", :id => 5}
#user_attr = { :username => "myname", :password => "mypass"}
post :create, :user => #user_attr, :league => #league_attr
assigns(:league_id).should eql(5)
end
end
end
But the test returns nil for the value of :league_id
I'm new to both programming in general and Rspec in particular, so I really appreciate any help someone might offer!
You cannot assign :id with new. Try this:
def create
#league = League.new(params[:league])
#league.id = params[:league][:id] if params[:league][:id]
#user = #league.users.build(params[:user])
#league_id = #league.id
#user.update_attribute('league_id', #league_id)
... .save conditions appended ...
end
That said, I wonder how come it works in the browser.
Also, you better off using FactoryGirl or Fixtures to assign data to models when testing.
Have you tried pry?
Whenever I discover something like this I find it very handy to be able to insert a brakepoint via Pry (or Ruby-Debug) so I can inspect the variables and their behavior.
I suspect putting in a binding.pry between #league_id = #league.id and #user.update_attribute('league_id', #league_id) may very well shed some light on the issue.
Also note that user will automatically inherit the #league_id when you persist it via the #league.save call. (that's the idea behind #league.users.build(..) - it will set the required relationships correctly upon persistance.

Stubbing model attributes in controller tests

I am finding it very hard to stub certain attributes of a model on a controller test. I want to make sure to stub as little as possible.
EDIT: I have been demoved of using stubs for such integration. I understood that the stubs won't reach the action call. The correct question would now be:
How can one use mocks and stubs to simulate a certain state in a Rails controller test?
So I've reached something like the following:
Spec
require 'spec_helper'
describe TeamsController do
let(:team) { FactoryGirl.create :team }
context "having questions" do
let(:competition) { FactoryGirl.create :competition }
it "allows a team to enter a competition" do
post(:enter_competition, id: team.id, competition_id: competition.id)
assigns(:enroll).team.should == team
assigns(:enroll).competition.should == competition
end
end
# ...
end
Factories
FactoryGirl.define do
factory :team do
name "Ruby team"
end
factory :competition, class: Competition do
name "Competition with questions"
after_create do |competition|
competition.
stub(:questions).
and_return([
"something"
])
end
end
factory :empty_competition, class: Competition do
name "Competition without questions"
questions []
after_create do |competition|
competition.stub(:questions).and_return []
end
end
end
Production code
class TeamsController < ApplicationController
def enter_competition
#team = Team.find params[:id]
#competition = Competition.find params[:competition_id]
#enroll = #team.enter_competition #competition
render :nothing => true
end
end
class Team < ActiveRecord::Base
def enter_competition competition
raise Competition::Closed if competition.questions.empty?
enroll = Enroll.new team: self, competition: competition
enroll.save
enroll
end
end
When I run the test, the questions attribute comes as being nil and so the test fails in the model when checking for nil.empty?.
Why isn't the stub being used so that the state of that message is correctly used? I expected that #competition.questions would be [ "question" ] but instead I get nil.
The problem you're running into is that stub works on an instance of a Ruby object; it doesn't affect all ActiveRecord objects that represent the same row.
The quickest way to fix your test would be to add this to your test, before the post:
Competition.stub(:find).and_return(competition)
The reason that's necessary is that Competition.find will return a fresh Competition object that doesn't have questions stubbed out, even though it represents the same database row. Stubbing find as well means that it will return the same instance of Competition, which means the controller will see the stubbed questions.
I'd advise against having that stub in your factory, though, because it won't be obvious what's stubbed as a developer using the factory, and because it means you'll never be able to test the real questions method, which you'll want to do in the Competition unit test as well as any integration tests.
Long story short: if you stub out a method on an instance of your model, you also need to stub out find for that model (or whatever class method you're using to find it), but it's not a good idea to have such stubs in a factory definition.
When you call create on FactoryGirl, it creates database records which you then retrieve back in your controller code. So the instances you get (#team, #competition) are pure ActiveRecord, without any methods stubbed out.
Personally I would write you test like this (not touching database at all):
let(:team) { mock_model(Team) }
let(:competition) { mock_model(Competition) }
before do
Team.stub(:find) { team }
Competition.stub(:find) { competition }
end
and then in your test something like this:
it "should call enter_competition on #team with #competition" do
team.should_receive(:enter_competition).with(competition)
post :enter_competition, id: 7, competition_id: 10
I don't really understand what your controller is supposed to do or what are you testing for that matter, sorry :(

Resources