如何独立测试模型的回调方法?(涉及IT技术)

42

我在一个模型中有一个方法:

class Article < ActiveRecord::Base
  def do_something
  end
end

我为这个方法编写了一个单元测试:

# spec/models/article_spec.rb
describe "#do_something" do
  @article = FactoryGirl.create(:article)
  it "should work as expected" do
    @article.do_something
    expect(@article).to have_something
  end
  # ...several other examples for different cases
end

在我发现将这个方法移动到after_save回调中更好之前,一切都很好:

class Article < ActiveRecord::Base
  after_save :do_something

  def do_something
  end
end

现在我所有有关这个方法的测试都失败了。我必须通过以下方式修复它:

  • 不再对do_something进行更为具体的调用,因为createsave也会触发该方法,否则将会遇到重复的数据库操作。
  • create改为build
  • 测试respond_to
  • 使用通用的model.save而不是单独的方法调用model.do_something

describe "#do_something" do
  @article = FactoryGirl.build(:article)
  it "should work as expected" do
    expect{@article.save}.not_to raise_error
    expect(@article).to have_something
    expect(@article).to respond_to(:do_something)
  end
end

测试通过,但我担心它不再只是关于特定的方法。如果添加更多回调,效果将与其他回调混合在一起

我的问题是,是否有一种优美的方式可以独立地测试模型的实例方法,而不成为回调函数?


不清楚为什么您原来的方法在测试时不起作用。直接对该方法进行单元测试,只需独立测试其作为回调函数的调用即可。我是否遗漏了什么或者您对这种方法有所不喜? - Andrew Hubbs
@AndrewHubbs,感谢您的提问。原因是这个方法改变了数据库。例如,它会将此文章分配到“Rails”类别中。重构为回调后,当我调用FactoryGirl.create时,此回调将生效并将文章分配给“Rails”类别。当我在测试中再次调用此方法时,会出现错误,因为它已经被分配了。 - Billy Chan
6个回答

89

回调和回调行为是独立的测试。如果您想检查一个 after_save 回调函数,你需要将其视为两个方面:

  1. 回调是否被正确触发?
  2. 被调用的函数是否执行了正确的操作?

假设您有一个具有许多回调的 Article 类,以下是如何进行测试:

class Article < ActiveRecord::Base
  after_save    :do_something
  after_destroy :do_something_else
  ...
end

it "triggers do_something on save" do
  expect(@article).to receive(:do_something)
  @article.save
end

it "triggers do_something_else on destroy" do
  expect(@article).to receive(:do_something_else)
  @article.destroy
end

it "#do_something should work as expected" do
  # Actual tests for do_something method
end

这将使您的回调与行为解耦。例如,当更新其他相关对象时,您可以触发相同的回调方法article.do_something,比如user.before_save { user.article.do_something }。这将适用于所有这些情况。

所以,像往常一样测试您的方法。把关注点放在回调上。

编辑:更正了错别字和潜在误解 编辑:将“做某事”改为“触发某事”


+1 是为了“将回调与行为解耦”。RDX,你对“# do_something方法的实际测试”有什么建议?我应该使用我的方法(通过“保存”对象来测试整体行为)吗? - Billy Chan
只需编写do_something方法的实际测试,类似于您帖子中的第一个rspec:@article.do_something; expect(@article).to have_something。基本上,save是一个较大的方法,可能会触发许多before_saves、after_saves等。但是,如果您已经测试了所有单独的方法,并且知道每个方法都正确执行其工作,则只需要为save/before_save/after_save/...方法编写非常少量和简短的测试即可。从经过良好测试的较小函数组合成更大的函数 :) - Subhas
你是否对以下问题有任何想法:http://stackoverflow.com/questions/35950470/rails-factorygirl-trait-association-with-model-after-create-callback-not-setting - Chris Hough

18
您可以使用shoulda-callback-matchers来测试回调的存在性,而无需调用它们。
describe Article do
  it { is_expected.to callback(:do_something).after(:save) }
end
如果您也希望测试回调函数的行为:
describe Article do
  ...

  describe "#do_something" do
    it "gives the article something" do
      @article.save
      expect(@article).to have_something
    end
  end
end

4
我喜欢使用ActiveRecord的#run_callbacks方法来确保回调被调用而无需访问数据库。这样可以使程序运行更快。
describe "#save" do
  let(:article) { FactoryBot.build(:article) }
  it "runs .do_something after save" do
    expect(article).to receive(:do_something)
    article.run_callbacks(:save)
  end
end

为测试 #do_something 方法的行为,您需要添加另一个专门针对该方法的测试。

describe "#do_something" do
  let(:article) { FactoryBot.build(:article) }
  it "return thing" do
    expect(article.do_something).to be_eq("thing")
  end
end

run_callbacks 只调用 before 和 around 回调函数,顺带一提。 - Rachel Lanman

3

完全同意。感谢分享这篇文章。 - fmquaglia

0

这更像是一条评论而不是答案,但我把它放在这里是为了语法高亮...

我想要一种跳过测试中回调的方法,这就是我所做的。(这可能有助于修复出现问题的测试。)

class Article < ActiveRecord::Base
  attr_accessor :save_without_callbacks
  after_save :do_something

  def do_something_in_db
    unless self.save_without_callbacks
      # do something here
    end
  end
end

# spec/models/article_spec.rb
describe Article do
  context "after_save callback" do
    [true,false].each do |save_without_callbacks|
      context "with#{save_without_callbacks ? 'out' : nil} callbacks" do
        let(:article) do
          a = FactoryGirl.build(:article)
          a.save_without_callbacks = save_without_callbacks
        end
        it do
          if save_without_callbacks
            # do something in db
          else
            # don't do something in db
          end
        end
      end
    end
  end
end

Teddy,谢谢你的回答或评论? :) 我担心这种方式的成本有点高,需要在测试中添加一些额外的代码,并故意修改模型以适应测试。 - Billy Chan
是的,我也需要填充数据库,在这种情况下跳过回调是必需的,所以我得到了更多的好处。 - Teddy
这个有一个skip_callback方法:https://github.com/rails/rails/blob/b894b7b90a6aced0e78ab84a45bf1c75c871bb2d/activesupport/lib/active_support/callbacks.rb#L620 - Vadym Chumel

-1
describe "#do_something" do

 it "gives the article something" do

  @article = FactoryGirl.build(:article)

   expect(@article).to have_something

 @article.save
end

end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接