部分跟随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类,因为我认为它的接口是外部的:“什么”而不是“如何”的一部分。)
答案 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()
生成输入,然后您忘记了两件事:
如果代码可以在逻辑上划分为这些函数,则将代码拆分为单独的函数总是一个好主意。它使代码更易于理解,更易于维护和更有效的可测试性。最后,因为:
即使您无法与output()
分开测试gather_data()
,您仍然可以单独测试gather_data()
。该测试将为您提供有关较小代码的早期警告,这样可以更轻松地识别和解决问题。
相关问题,您认为无法与output()
分开测试gather_data()
,这不是一个通常可以解决的问题。在每种情况下,您都有三种选择:
您模拟了output()
的文字输入,并根据该输入(通常是一些变体,以测试多个代码路径)以孤立的方式对其进行测试。
你编写了一个模拟版本的gather_data(),它部分复制了它的逻辑,但更容易推理并调用那个来为output()
生成输入。
您一起测试gather_data()
和output()
,因为实际上单独测试output()
太麻烦了。
gather_data()
需要输入,您可以手动提供,也可以通过脚本或生成它的代码提供。最后一个是完全可以接受的实用解决方案,假设您对gather_data()
进行了单独测试,该测试已经告诉您合并测试是否因gather_data()
失败或output()
失败而失败。