How do I write short, clean rspec tests for a method with many model calls?
I am having problems with some tests for a method I want to write.
This method will take a hash of some data and create a bunch of related models with it. The problem is that I am having a hard time determining what is best for writing such a test.
For example, the code would be:
Take a hash that looks like this:
{
:department => 'CS',
:course_title => 'Algorithms',
:section_number => '01B'
:term => 'Fall 2012',
:instructor => 'Bob Dylan'
}
And save it in the models Department
, Course
, Section
and Instructor
.
It will take a lot of calls model.find_or_create
, etc.
How can I go about testing each individual target of this method, for example:
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 amount of stubs so that every test is FIRST (fast independent repeatable self-test in a timely manner)? Is there a better way to write this method and / or these tests? I would rather have short, clean blocks it
.
Thank you so much for any help.
Edit: This 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 a method? How should I write tests? Thank!
source to share
To pass an array of sections to create:
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
Hope this covers it all.
source to share
My first thought is that you may be suffering from a larger design flaw. Without seeing more context for your method, it's hard to give a lot of advice. However, in general, it is useful to break the method down into smaller chunks and follow the principle of a single level of abstraction.
http://www.markhneedham.com/blog/2009/06/12/coding-single-level-of-abstraction-principle/
Here's what you could try, although as mentioned earlier, it's definitely still not perfect:
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 specification ...
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 them helped a little. If you post more of your example code, I can provide less generalized and possibly helpful advice.
source to share
Given the new context, I would like to split the functionality a bit among your models. Again, this is really just the first thing that comes to mind and can certainly be improved. It seems to me that Section
is the root object. So you can either add a method Section.create_course
or wrap it in a service object like this:
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
Note that adding a method #find_or_create_course_with_department
allowed us to add a department association there, leaving the method #add_course
pure. This is why I like to add these methods, even if they sometimes seem trivial, as in the case of a method #find_or_create_instructor
.
Another advantage of developing methods this way is that they become easier to stub out in tests, as I showed in my first example. You can easily stub all of these methods to ensure that the database hasn't actually been hit and your tests run quickly and at the same time ensure that the test expectations match functionality.
Of course, a lot of it comes down to personal preference as to how you want to implement it. In this case, the service object is probably not needed. You could just as easily implement this as the method Section.create_course
I referred to earlier:
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 for your final question, you can definitely stub the methods in RSpec and then apply the same expectations to them as should_receive
.
It's getting late for me, so let me know if I missed something.
source to share