我正在全力以赴地围绕Rspec,以便更多地转向TDD / BDD开发模式。但是,我还有很长的路要走,并且在一些基本面上挣扎:
就像,我应该何时使用模拟/存根,何时不应该使用?
以此方案为例:我有一个Site
模型has_many :blogs
和Blog
模型has_many :articles
。在我的Site
模型中,我有一个回调过滤器,可以为每个新站点创建一组默认的博客和文章。我想测试那段代码,所以这里是:
describe Site, "when created" do
include SiteSpecHelper
before(:each) do
@site = Site.create valid_site_attributes
end
it "should have 2 blogs" do
@site.should have(2).blogs
end
it "should have 1 main blog article" do
@site.blogs.find_by_slug("main").should have(1).articles
end
it "should have 2 secondary blog articles" do
@site.blogs.find_by_slug("secondary").should have(2).articles
end
end
现在,如果我运行该测试,一切都会过去。然而,它也很慢,因为它为每一次测试创建一个新网站,两个新博客和三个新文章!所以我想知道,这是否适合使用存根?我们试一试:
describe Site, "when created" do
include SiteSpecHelper
before(:each) do
site = Site.new
@blog = Blog.new
@article = Article.new
Site.stub!(:create).and_return(site)
Blog.stub!(:create).and_return(@blog)
Article.stub!(:create).and_return(@article)
@site = Site.create valid_site_attributes
end
it "should have 2 blogs" do
@site.stub!(:blogs).and_return([@blog, @blog])
@site.should have(2).blogs
end
it "should have 1 main blog article" do
@blog.stub!(:articles).and_return([@article])
@site.stub_chain(:blogs, :find_by_slug).with("main").and_return(@blog)
@site.blogs.find_by_slug("main").should have(1).articles
end
it "should have 2 secondary blog articles" do
@blog.stub!(:articles).and_return([@article, @article])
@site.stub_chain(:blogs, :find_by_slug).with("secondary").and_return(@blog)
@site.blogs.find_by_slug("secondary").should have(2).articles
end
end
现在所有测试仍然通过,事情也有点快。但是,我的测试时间增加了一倍,整个练习对我来说完全毫无意义,因为我不再测试我的代码了,我只是测试我的测试。
现在,要么我完全错过了模拟/存根,或者我从根本上接近它的错误,但我希望有人能够:
答案 0 :(得分:2)
但是,我的测试时间增加了一倍,整个练习对我来说完全毫无意义,因为我不再测试我的代码了,我只是测试我的测试。
这是关键。不测试代码的测试没有用。如果您可以负面地更改测试应该测试的代码,并且测试不会失败,那么它们就不值得拥有。
根据经验,除非必须,否则我不喜欢模拟/存根。例如,当我正在编写控制器测试时,我想确保在记录无法保存时发生相应的操作,我发现更容易将对象的save
方法存根以返回false,而不是精心设计参数只是为了确保模型无法保存。
另一个示例是名为admin?
的帮助程序,它根据当前登录用户是否为管理员而返回true或false。我不想伪造用户登录,所以我这样做了:
# helper
def admin?
unless current_user.nil?
return current_user.is_admin?
else
return false
end
end
# spec
describe "#admin?" do
it "should return false if no user is logged in" do
stubs(:current_user).returns(nil)
admin?.should be_false
end
it "should return false if the current user is not an admin" do
stubs(:current_user).returns(mock(:is_admin? => false))
admin?.should be_false
end
it "should return true if the current user is an admin" do
stubs(:current_user).returns(mock(:is_admin? => true))
admin?.should be_true
end
end
作为中间立场,您可能需要查看Shoulda。通过这种方式,您可以确保您的模型具有定义的关联,并且相信Rails已经过充分测试,以至于关联将“正常工作”而无需您创建关联模型然后对其进行计数
我有一个名为Member
的模型,基本上我应用中的所有内容都与之相关。它定义了10个关联。我可以测试每个关联,或者我可以这样做:
it { should have_many(:achievements).through(:completed_achievements) }
it { should have_many(:attendees).dependent(:destroy) }
it { should have_many(:completed_achievements).dependent(:destroy) }
it { should have_many(:loots).dependent(:nullify) }
it { should have_one(:last_loot) }
it { should have_many(:punishments).dependent(:destroy) }
it { should have_many(:raids).through(:attendees) }
it { should belong_to(:rank) }
it { should belong_to(:user) }
it { should have_many(:wishlists).dependent(:destroy) }
答案 1 :(得分:1)
这正是我很少使用存根/模拟的原因(实际上只有在我打算使用外部Web服务时)。节省的时间不值得增加复杂性。
有更好的方法可以加快您的测试时间,Nick Gauthier提供了很好的讨论,涵盖了一大堆 - 请参阅video和slides。
另外,我认为一个很好的选择是为您的测试运行尝试内存中的sqlite数据库。这应该可以减少你的数据库时间,而不必为了所有内容而不必访问磁盘。我自己没有尝试过这个(我主要使用具有相同优点的MongoDB),所以请谨慎行事。 Here's最新的博客文章。
答案 2 :(得分:1)
我不太确定与其他人达成一致意见。这里真正的问题(我认为)是你用同样的测试(发现行为和创造)测试多个有趣的行为。有关为什么会这么糟糕的原因,请参阅此演讲:http://www.infoq.com/presentations/integration-tests-scam。我假设你想要测试创建是你想要测试的其余部分。
隔离主义测试通常看起来很笨拙,但这通常是因为他们有设计课程来教你。下面是我可以看到的一些基本的东西(虽然没有看到生产代码,但我不能做太多好事。)
对于初学者来说,要查询设计,让Site
向博客添加文章是否有意义? Blog
上的类方法如何称为Blog.with_one_article
。这意味着您必须测试的是,该类方法已被调用两次(如果[我现在理解],则每个Blog
都有一个“主要”和“次要”Site
并且已建立关联(我还没有找到一种在rails中执行此操作的好方法,我通常不对其进行测试)。
此外,您在致电Site.create
时是否重写了ActiveRecord的创建方法?如果是这样,我建议在Site上创建一个名为else的新类方法(Site.with_default_blogs
可能吗?)。这只是我的一般习惯,压倒性的东西通常会在以后的项目中引起问题。