我无法为我想写的方法进行一些测试。
该方法将采用一些数据的哈希值并用它创建一堆相关的模型。问题是,我很难弄清楚编写此类测试的最佳实践是什么。
例如,代码将:
采用看起来像的哈希:
{
:department => 'CS',
:course_title => 'Algorithms',
:section_number => '01B'
:term => 'Fall 2012',
:instructor => 'Bob Dylan'
}
并将其保存到模型Department
,Course
,Section
和Instructor
。
这将需要多次调用model.find_or_create
等等。
我怎样才能测试这种方法的每个单独目的,例如:
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
有没有办法避免大量的存根来保持每个测试的最佳状态(及时快速独立的可重复自检)?是否有更好的方法来编写此方法和/或这些测试?我真的更喜欢短暂,干净的it
块。
非常感谢您的帮助。
编辑: 该方法可能如下所示:
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
有没有更好的方法来编写方法?我该怎么写测试?谢谢!
答案 0 :(得分:1)
我的第一个想法是你可能会遇到更大的设计缺陷。如果没有看到你的方法的更大背景,很难给出很多建议。但是,一般来说,最好将方法分解为更小的部分,并遵循单一的抽象原则。
http://www.markhneedham.com/blog/2009/06/12/coding-single-level-of-abstraction-principle/
这是你可以尝试的东西,尽管如前所述,这绝对不是理想的:
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.
在规范中......
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
希望其中一些有所帮助。如果您发布更多示例代码,我可能会提供不那么通用且可能有用的建议。
答案 1 :(得分:1)
鉴于提供了新的上下文,我会将您的模型中的功能分开一些。再次,这真的只是我想到的第一件事,并且肯定可以改进。在我看来,Section
就是这里的根对象。因此,您可以添加Section.create_course
方法或将其包装在service object中,如下所示:
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
注意添加#find_or_create_course_with_department
方法如何允许我们在保持#add_course
方法清洁的同时添加部门的关联。这就是为什么我喜欢添加这些方法,即使它们有时看起来像是#find_or_create_instructor
方法那样过于夸张。
以这种方式突破方法的另一个好处是,如我在第一个例子中所示,它们在测试中变得更容易存根。您可以轻松地存储所有这些方法,以确保数据库实际上没有被命中,并且您的测试运行得很快,同时通过测试期望保证功能正确。
当然,很多问题归结为个人偏好你想要如何实现它。在这种情况下,服务对象可能是不必要的。您可以像我之前引用的Section.create_course
方法一样轻松地实现它:
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
关于你的最后一个问题,你绝对可以在RSpec中删除方法,然后在这些存根之上应用should_receive
之类的期望。
现在已经很晚了,如果我遗漏了任何东西,请告诉我。
答案 2 :(得分:1)
用于传递要创建的部分数组:
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
希望这涵盖了所有内容。