有什么方法可以避免在使用模拟时将许多测试合并到一个示例中?

时间:2011-07-08 16:28:56

标签: ruby tdd rspec

部分跟随this问题。希望这个例子说明一切:有一个WishlistReporter类,它要求一个对象提供数据并输出到另一个对象。

问题在于,对于DB的双倍,我实际上是在一个例子中测试了很多东西。哪个不理想。

我可以将report()方法拆分为gather_data()和output()方法。但这没有帮助:为了测试output()方法,我仍然需要创建模拟数据库并再次运行gather_data()。

有解决方法吗?

describe WishlistReporter do

  it "should talk to the DB and output a report" do
    db = double("database")
    db.should_receive(:categories).and_return(["C1"])
    db.should_receive(:items).with("C1").and_return(["I1", "I2"])
    db.should_receive(:subitems).with("I1").and_return(["S1", "S2"])
    db.should_receive(:subitems).with("I2").and_return(["S3", "S4"])

    wr = StringIO.new

    r = WishlistReporter.new(db)
    r.report(db, :text, wr)

    wr.seek(0)
    wr.read.should =~ /stuff/
  end
end

(参考上一个问题:我很高兴模仿Db类,因为我认为它的接口是外部的:“什么”而不是“如何”的一部分。)

4 个答案:

答案 0 :(得分:1)

我总是将这种期望添加到前一块。我会像这样编写你的规范:

describe WishlistReporter do
  let(:db) { double('database') }
  let(:wf) { StringIO.new }

  subject { WishListReporter.new(db) }

  describe '#read' do
    before do
      db.should_receive(:categories).and_return(["C1"])
      db.should_receive(:items).with("C1").and_return(["I1", "I2"])
      db.should_receive(:subitems).with("I1").and_return(["S1", "S2"])
      db.should_receive(:subitems).with("I2").and_return(["S3", "S4"])

      subject.report(db, :text, wr)
      subject.seek(0)
    end

    it 'talks to the DB and outputs a report' do
      subject.read.should =~ /stuff/
    end
  end
end

答案 1 :(得分:1)

在这种情况下,我不会验证对@db的调用,因为这些是只读调用,所以我真的不在乎它们是否发生。是的,当然它们确实必须发生(否则来自哪里的数据),但我不认为它是对WishlistReporter行为的明确要求......如果WishlistReporter可以在不与数据库交谈的情况下生成报告那对我来说完全没问题。

我会使用db.stub!代替db.should_receive,并对此非常满意。

但是对于被模拟对象的调用具有副作用且明确要求的情况,我会做这样的事情。 (在此示例中,无论出于何种原因,我们都要求在我们查询之前指示db对象重新加载其数据。)同样,不需要显式验证返回数据的方法,因为如果报告输出是正确,那么数据必须从@db正确拉出:

describe WishlistReporter do

  before(:each) do
    @db = double("database")
    @db.stub!(:reload_data_from_server)
    @db.stub!(:categories).and_return(["C1"])
    @db.stub!(:items).with("C1").and_return(["I1", "I2"])
    @db.stub!(:subitems).with("I1").and_return(["S1", "S2"])
    @db.stub!(:subitems).with("I2").and_return(["S3", "S4"])
    @output = StringIO.new
    @subject = WishlistReporter.new(@db)
  end

  it "should reload data before generating a report" do
    @db.should_receive(:reload_data_from_server)

    @subject.report(:text, @output)
  end

  it "should output a report" do
    @subject.report(:text, @output)

    @output.seek(0)
    @output.read.should =~ /stuff/
  end
end

答案 2 :(得分:0)

再次回答我自己的问题!这是解决问题的一种方法,但坦率地说,对我而言,治愈方法比疾病更糟糕。

describe WishlistReporter do
  before(:each) do
    @db = double("database")
    @wr = WishlistReporter.new(db)
  end

  describe "#gather_data" do        
    it "should talk to the DB" do
      @db.should_receive(:categories).and_return(["C1"])
      @db.should_receive(:items).with("C1").and_return(["I1", "I2"])
      @db.should_receive(:subitems).with("I1")
      @db.should_receive(:subitems).with("I2")
      @wr.gather_data
    end
  end

  describe "#report" do
    it "should output a report" do
      @wr.should_receive(:gather_data).and_return( {"C1"=>{"I1"=>["S1", "S2"], "I2"=> etc etc}})
      file = StringIO.new

      @wr.report(:text, file)

      wr.seek(0)
      wr.read.should =~ /stuff/
    end
  end

end

我认为这里的重点是我刚刚在代码中引入了大量复杂性,以使示例更简单一些。这不是我感到舒服的权衡。

答案 3 :(得分:-1)

也许这不是重点,但我认为你的代码是以一种奇怪的方式构建的。而不是期望代码调用

categories = db.categories
categories.each do |category| 
  items = db.items(category) 
  items.each do |item|
    db.subitems(item)
  end
end

我希望它能打电话:

categories = db.categories
categories.each do |category| 
  items = category.items
  items.each do |item|
    item.subitems
  end
end

类别对象包含项目。您不需要将类别对象传递给db对象来获取其项目。要么您没有使用ActiveRecord(或DataMapper,或......),要么以奇怪的方式使用它。

然后你可以这样做:

let(:items) {[mock('item1', :subitems => ["S1", "S2"]), 
              mock('item2', :subitems => ["S3", "S4"])]}
let(:categories) {[mock('category', :items => items)]}
let(:db) {double('database', :categories => categories)}

避免必须命名所有内容并在所有规范之间共享存根。这些是存根,而不是期望,但由于这不是您正在测试的核心功能,我同意Aaron V.认为存根更合适。


在回复评论并重新阅读问题后进行编辑:

您在问题中的主要抱怨是

  

使用DB的双倍,我实际上是在一个例子中测试了很多东西

对我而言,表明你的问题是双重问题,因为这是你“测试一堆东西”的原因。也许你不喜欢期望(这可能是测试将测试与实现紧密结合的事物的方式),链接列表以使其工作或感知需要重复它们。所有这些都是有道理的,你自己的答案,使用链式期望,反映了双重使用方式的简化愿望。

然而,当再次仔细阅读时,实际问题是将一些方法分成另外两种方法并分别进行测试。不幸的是,这个例子根本不支持这种推理。它与双重或期望无关,你可以毫无问题地将它们排除在外。您对代码和问题非常熟悉,这对您来说似乎都很明显,但对我而言,作为您问题的随意读者,并非如此。

现在关于你的问题:如果你的问题确实存在,你认为将report()分成两个函数毫无意义,因为你无法单独测试output() gather_data(),因为需要调用gather_data()来为output()生成输入,然后您忘记了两件事:

  1. 如果代码可以在逻辑上划分为这些函数,则将代码拆分为单独的函数总是一个好主意。它使代码更易于理解,更易于维护和更有效的可测试性。最后,因为:

  2. 即使您无法与output()分开测试gather_data(),您仍然可以单独测试gather_data()。该测试将为您提供有关较小代码的早期警告,这样可以更轻松地识别和解决问题。

  3. 相关问题,您认为无法与output()分开测试gather_data(),这不是一个通常可以解决的问题。在每种情况下,您都有三种选择:

    1. 您模拟了output()的文字输入,并根据该输入(通常是一些变体,以测试多个代码路径)以孤立的方式对其进行测试。

    2. 你编写了一个模拟版本的gather_data(),它部分复制了它的逻辑,但更容易推理并调用那个来为output()生成输入。

    3. 您一起测试gather_data()output(),因为实际上单独测试output()太麻烦了。

    4. gather_data()需要输入,您可以手动提供,也可以通过脚本或生成它的代码提供。最后一个是完全可以接受的实用解决方案,假设您对gather_data()进行了单独测试,该测试已经告诉您合并测试是否因gather_data()失败或output()失败而失败。