3

我无法为我想编写的方法提出一些测试。

该方法将获取一些数据的散列并用它创建一堆关联的模型。问题是,我很难弄清楚编写此类测试的最佳实践是什么。

例如,代码将:

取一个看起来像这样的哈希:

{
  :department => 'CS',
  :course_title => 'Algorithms',
  :section_number => '01B'
  :term => 'Fall 2012',
  :instructor => 'Bob Dylan'
}

并将其保存到模型DepartmentCourseSectionInstructor.

这将需要多次调用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

有没有更好的方法来编写方法?我将如何编写测试?谢谢!

4

3 回答 3

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

希望其中一些有所帮助。如果您发布更多示例代码,我可能能够提供不太笼统且可能有用的建议。

于 2013-02-13T02:08:47.000 回答
1

鉴于提供的新上下文,我将在您的模型中进一步拆分功能。同样,这实际上只是首先想到的,并且绝对可以改进。在我看来,这Section是这里的根对象。因此,您可以添加一个Section.create_course方法或将其包装在一个服务对象中,如下所示:

更新了此示例以使用异常

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在这些存根之上应用期望。

现在已经很晚了,所以如果我错过了什么,请告诉我。

于 2013-02-13T06:14:28.613 回答
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

希望这涵盖了所有内容。

于 2013-02-13T06:35:04.383 回答