【问题标题】:How to test model's callback method independently?如何独立测试模型的回调方法?
【发布时间】:2013-05-21 19:12:54
【问题描述】:

我在模型中有一个方法:

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
    

测试通过了,但我担心它不再是具体的方法。 如果添加更多,效果将与其他回调混合

我的问题是,有没有什么漂亮的方法可以独立测试模型的实例方法,使其成为回调?

【问题讨论】:

  • 尚不清楚为什么您的原始方法仍不能用于测试。直接对方法进行单元测试,并仅测试它是否被独立地作为回调调用。是我遗漏了什么,还是您不喜欢这种方法?
  • @AndrewHubbs,感谢您的提问。原因是这种方法改变了 db.例如,它将这篇文章分配给类别“Rails”。重构为回调后,当我调用FactoryGirl.create时,该回调将生效并将文章分配到“Rails”类别。当我在测试中再次调用此方法时,会出现错误,因为它已经分配了。

标签: ruby-on-rails ruby-on-rails-3 activerecord rspec factory-bot


【解决方案1】:

回调和回调行为是独立的测试。如果要检查 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 方法的实际测试”有什么建议?稍后使用我的方法(通过“保存”对象来测试整体行为)?
  • 只是do_something 方法的实际测试,有点像您帖子中的第一个rspec:@article.do_something; expect(@article).to have_something。基本上save 是一个更大的方法,它可能会触发很多 before_saves、after_saves 等。但是如果你已经测试了所有单独的方法,并且你知道每个方法都在正确地完成它的工作,那么你只需要编写save/before_save/after_save/... 方法的测试非常少和简短。用经过良好测试的小函数组成更大的函数:)
  • 碰巧,你对stackoverflow.com/questions/35950470/…有什么想法
【解决方案2】:

您可以使用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

【讨论】:

    【解决方案3】:

    我喜欢使用 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 回调 fwiw
    【解决方案4】:

    本着 Sandi Metz 和极简主义 testing 的精神,https://stackoverflow.com/a/16678194/2001785 中确认调用可能是私有方法的建议对我来说似乎不正确。

    测试可公开观察的副作用或确认传出的命令消息对我来说更有意义。 Christian Rolle 在http://www.chrisrolle.com/en/blog/activerecord-callback-tests-with-rspec 提供了一个示例。

    【讨论】:

    • 不能再同意了。感谢您分享文章。
    【解决方案5】:

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

    我想要一种方法来跳过测试中的回调,这就是我所做的。 (这可能有助于测试失败。)

    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
    

    【讨论】:

    • 泰迪,感谢您的回答,或评论? :) 这种方式恐怕成本有点高,需要额外的代码在测试中,并且故意改变模型以适应测试。
    • 是的,我还必须为数据库播种,其中跳过回调是必需的,所以我的情况得到了更多好处。
    • 有一个skip_callback方法:github.com/rails/rails/blob/…
    【解决方案6】:
    describe "#do_something" do
    
     it "gives the article something" do
    
      @article = FactoryGirl.build(:article)
    
       expect(@article).to have_something
    
     @article.save
    end
    
    end
    

    【讨论】:

      猜你喜欢
      • 2023-02-26
      • 1970-01-01
      • 2018-04-20
      • 2022-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多