简体   繁体   English

什么时候以及什么时候不存根/模拟

[英]When and when not to stub/mock a test

I am making a concerted effort to wrap my head around Rspec in order to move towards more of a TDD/BDD development pattern. 我正在齐心协力围绕Rspec努力,以朝着更多的TDD / BDD开发模式迈进。 However, I'm a long way off and struggling with some of the fundamentals: 但是,我还有很长一段路要走,并且在一些基本原则上苦苦挣扎:

Like, when exactly should I be using mocks/stubs and when shouldn't I? 就像,什么时候应该使用模拟/存根,什么时候不应该使用?

Take for example this scenario: I have a Site model that has_many :blogs and the Blog model has_many :articles . 以这种情况为例:我有一个has_many :blogsSite模型,而Blog模型has_many :articles In my Site model I have a callback filter that creates a default set of blogs and articles for every new site. 在我的Site模型中,我有一个回调过滤器,该过滤器为每个新网站创建默认的博客和文章集。 I want to test that code, so here goes: 我想测试该代码,所以去了:

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

Now, if I run that test, everything passes. 现在,如果我运行该测试,一切都会通过。 However, it's also pretty slow as it's creating a new Site, two new Blogs and three new Articles - for every single test! 但是,它也很慢,因为它为每个测试都创建了一个新站点,两个新Blog和三个新文章! So I wonder, is this a good candidate for using stubs? 所以我想知道,这是使用存根的好候选人吗? Let's give it a go: 让我们开始吧:

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

Now all the tests still pass, and things are a bit speedier too. 现在,所有测试仍然通过,而且速度也有所提高。 But, I've doubled the length of my tests and the whole exercise just strikes me as utterly pointless, because I'm no longer testing my code, I'm just testing my tests. 但是,我的测试时间增加了一倍,而整个练习却毫无意义,因为我不再测试代码,而只是测试了。

Now, either I've completely missed the point of mocks/stubs, or I'm approaching it fundamentally wrong, but I'm hoping someone might be able to either: 现在,或者我完全错过了模拟/存根的要点,或者我从根本上解决了这个问题,但是我希望有人能够做到:

  • Improve me tests above so it uses stubs or mocks in a way that actually tests my code, rather than my tests. 改进上面的测试,以便它以实际测试我的代码而不是测试的方式使用存根或模拟。
  • Or, tell me if I should even be using stubs here - or whether in fact this is completely unnecessary and I should be writing these models to the test database. 或者,告诉我我是否应该在这里使用存根-或实际上是否完全没有必要,我应该将这些模型写入测试数据库。

But, I've doubled the length of my tests and the whole exercise just strikes me as utterly pointless, because I'm no longer testing my code, I'm just testing my tests. 但是,我的测试时间增加了一倍,而整个练习却毫无意义,因为我不再测试代码,而只是测试了。

This is the key right here. 这就是这里的关键。 Tests that don't test your code aren't useful. 不测试代码的测试没有用。 If you can negatively change the code that your tests are supposed to be testing, and the tests don't fail, they're not worth having. 如果您可以否定地更改您应该测试的代码,并且测试不会失败,则不值得拥有。

As a rule of thumb, I don't like to mock/stub anything unless I have to. 根据经验,除非有必要,否则我不喜欢模拟/存根任何内容。 For example, when I'm writing a controller test, and I want to make sure that the appropriate action happens when a record fails to save, I find it easier to stub the object's save method to return false, rather than carefully crafting parameters just so in order to make sure a model fails to save. 例如,当我编写控制器测试时,我想确保在记录保存失败时发生适当的动作,我发现将对象的save方法存根以返回false更容易,而不是仅仅精心设计参数因此,为了确保模型无法保存。

Another example is for a helper called admin? 另一个示例是一个名为admin?的助手admin? that just returns true or false based on whether or not the currently logged-in user is an admin or not. 会根据当前登录的用户是否是管理员来返回true或false。 I didn't want to go through faking a user login, so I did this: 我不想伪造用户登录,所以我这样做了:

# 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

As a middle ground, you might want to look into Shoulda . 作为中间立场,您可能需要研究Shoulda This way you can just make sure your models have an association defined , and trust that Rails is well-tested enough that the association will "just work" without you having to create an associated model and then counting it. 这样,您只需确保模型已定义了关联,并相信Rails已经过充分的测试,因此关联将“正常工作”,而无需创建关联模型然后对其进行计数。

I've got a model called Member that basically everything in my app is related to. 我有一个名为Member的模型,基本上我的应用程序中的所有内容都与之相关。 It has 10 associations defined. 它定义了10个关联。 I could test each of those associations, or I could just do this: 我可以测试每个关联,也可以这样做:

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) }

This is exactly why I use stubs/mocks very rarely (really only when I'm going to be hitting an external webservice). 这就是为什么我很少使用存根/模拟的原因(仅当我要使用外部Web服务时才使用)。 The time saved just isn't worth the added complexity. 节省的时间不值得增加额外的复杂性。

There are better ways to speed up your testing time, and Nick Gauthier gives a good talk covering a bunch of them - see the video and the slides . 有更好的方法可以加快测试时间,Nick Gauthier很好地介绍了很多方法,请参见视频幻灯片

Also, I think a good option is to try out an in-memory sqlite database for your test runs. 另外,我认为一个不错的选择是尝试使用内存中的sqlite数据库进行测试。 This should cut down on your database time by quite a bit by not having to hit the disk for everything. 这样就不必在磁盘上存放所有东西,从而可以大大减少数据库时间。 I haven't tried this myself, though (I primarily use MongoDB, which has the same benefit), so tread carefully. 不过,我自己还没有尝试过(我主要使用MongoDB,它具有相同的好处),因此请谨慎操作。 Here's a fairly recent blog post on it. 这是一篇有关它的近期博客文章。

I'm not so sure with agreeing on the others. 我不太同意其他人。 The real problem (as I see it) here, is that you're testing multiple pieces of interesting behavior with the same tests (the finding behavior, and the creation). 真正的问题(如我所见)是,您正在使用相同的测试(发现行为和创建)测试多个有趣的行为。 For reasons on why this is bad, see this talk: http://www.infoq.com/presentations/integration-tests-scam . 有关导致此问题的原因的详细信息,请参见此讨论: http : //www.infoq.com/presentations/integration-tests-scam I'm assuming for the rest of this answer that you want to test that creation is what you want to test. 对于此答案的其余部分,我假设您要测试创建的内容就是您要测试的内容。

Isolationist tests often seem unwieldy, but that's often because they have design lessons to teach you. 隔离测试似乎很笨拙,但这通常是因为它们有设计课程来教您。 Below are some basic things I can see out of this (though without seeing the production code, I can't do too much good). 以下是我可以从中看到的一些基本内容(尽管没有看到生产代码,但是我做不到太多的事情)。

For starters, to query the design, does having the Site add articles to a blog make sense? 首先,要查询设计,让Site向博客添加文章是否有意义? What about a class method on Blog called something like Blog.with_one_article . Blog上的类方法Blog.with_one_articleBlog.with_one_article This then means all you have to test is that that class method has been called twice (if [as I understand it for now], you have a "primary" and "secondary" Blog for each Site , and that the associations are set up (I haven't found a great way to do this in rails yet, I usually don't test it). 然后,这意味着您需要测试的是该类方法已被调用两次(如果[据我目前所知],则每个Site都有一个“主” Blog和“辅助” Blog ,并且已经建立了关联(我还没有找到在Rails中执行此操作的好方法,我通常不对其进行测试)。

Furthermore, are you overriding ActiveRecord's create method when you call Site.create ? 此外,您在调用Site.create时是否覆盖ActiveRecord的create方法? If so, I'd suggest making a new class method on Site named something else ( Site.with_default_blogs possibly?). 如果是这样,我建议在Site上创建一个新的类方法,命名为其他名称(可能是Site.with_default_blogs ?)。 This is just a general habit of mine, overriding stuff generally causes problems later on in projects. 这只是我的一个普遍习惯,过多的东西通常会在以后的项目中引起问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 使用存根运行rspec测试时的未定义方法 - Undefined Method when running rspec test using a stub 在测试环境中如何干净地存根REST客户端 - How to cleanly stub out REST client when in test environment 如何在不使用rspec将测试耦合到测试代码的情况下进行存根/模拟? - How to stub/mock without coupling the test to the code under test with rspec? 在 Ruby on Rails 应用程序中使用 RSpec 时,存根 Mock 对象的 #class 方法是否合法? - Is it legal to stub the #class method of a Mock object when using RSpec in a Ruby on Rails application? 如何在Rails 2.3中存根/模拟一个辅助方法进行功能测试? - How to stub/mock a helper method for functional test in Rails 2.3? 如何测试double,mock或stub控制器端对象 - How to test double, mock or stub controller side object Rails 5中没有提供块时如何跳过/模拟测试 - How to skip / mock a test when no block is provided in Rails 5 尝试测试调用 AWS S3 Bucket copy_to 的 (minitest) 方法。 如何模拟或存根? - Trying to test (minitest) method that invokes AWS S3 Bucket copy_to. How to mock or stub? 模拟一个Active Record抽象类以及如何在rails test :: unit / mocha中存根一个nil对象? - Mock a Active Record abstract class and how to stub a nil object in rails test::unit/mocha? 如何使用Test :: Unit在Rails中存根/模拟original_filename方法 - How to stub/mock original_filename method in Rails using Test::Unit
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM