似乎有两种完全不同的测试方法,我想引用它们。
问题是,这些观点是 5 年前(2007 年)提出的,我很感兴趣,从那时起发生了什么变化以及我应该走哪条路。
理论上,测试应该与实现无关。这导致不那么脆弱的测试并实际测试结果(或行为)。
使用 RSpec,我觉得完全模拟模型来测试控制器的常用方法最终会迫使你过多地关注控制器的实现。
这本身并不算太糟糕,但问题是它过多地关注控制器来决定如何使用模型。为什么我的控制器调用 Thing.new 很重要?如果我的控制器决定采用 Thing.create!和救援路线?如果我的模型有一个特殊的初始化方法,比如 Thing.build_with_foo?如果我更改实现,我的行为规范不应该失败。
当您有嵌套资源并且为每个控制器创建多个模型时,这个问题会变得更糟。我的一些设置方法最终会长达 15 行或更多行并且非常脆弱。
RSpec 的目的是将您的控制器逻辑与您的模型完全隔离开来,这在理论上听起来不错,但对于像 Rails 这样的集成堆栈来说,这几乎是违反规定的。特别是如果你练习瘦控制器/胖模型纪律,控制器中的逻辑量会变得非常小,而设置会变得巨大。
那么 BDD 想要做什么呢?退后一步,我真正想要测试的行为不是我的控制器调用 Thing.new,而是给定参数 X,它会创建一个新事物并重定向到它。
大卫切利姆斯基:
这都是关于权衡的。
The fact that AR chooses inheritance rather than delegation puts us in a testing bind – we have to be coupled to the database OR we have to be more intimate with the implementation. We accept this design choice because we reap benefits in expressiveness and DRY-ness.
In grappling with the dilemma, I chose faster tests at the cost of slightly more brittle. You’re choosing less brittle tests at the cost of them running slightly slower. It’s a trade-off either way.
In practice, I run the tests hundreds, if not thousands, of times a day (I use autotest and take very granular steps) and I change whether I use “new” or “create” almost never. Also due to granular steps, new models that appear are quite volatile at first. The valid_thing_attrs approach minimizes the pain from this a bit, but it still means that every new required field means that I have to change valid_thing_attrs.
But if your approach is working for you in practice, then its good! In fact, I’d strongly recommend that you publish a plugin with generators that produce the examples the way you like them. I’m sure that a lot of people would benefit from that.
Out of curiosity, how often do you use mocks in your tests/specs? Perhaps I'm doing something wrong, but I'm finding it severely limiting. Since switching to rSpec over a month ago, I've been doing what they recommend in the docs where the controller and view layers do not hit the database at all and the models are completely mocked out. This gives you a nice speed boost and makes some things easier, but I'm finding the cons of doing this far outweigh the pros. Since using mocks, my specs have turned into a maintenance nightmare. Specs are meant to test the behavior, not the implementation. I don't care if a method was called I just want to make sure the resulting output is correct. Because mocking makes specs picky about the implementation, it makes simple refactorings (that don't change the behavior) impossible to do without having to constantly go back and "fix" the specs. I'm very opinionated about what a spec/tests should cover. A test should only break when the app breaks. This is one reason why I hardly test the view layer because I find it too rigid. It often leads to tests breaking without the app breaking when changing little things in the view. I'm finding the same problem with mocks. On top of all this, I just realized today that mocking/stubbing a class method (sometimes) sticks around between specs. Specs should be self contained and not influenced by other specs. This breaks that rule and leads to tricky bugs. What have I learned from all this? Be careful where you use mocking. Stubbing is not as bad, but still has some of the same issues.
I took the past few hours and removed nearly all mocks from my specs. I also merged the controller and view specs into one using "integrate_views" in the controller spec. I am also loading all fixtures for each controller spec so there's some test data to fill the views. The end result? My specs are shorter, simpler, more consistent, less rigid, and they test the entire stack together (model, view, controller) so no bugs can slip through the cracks. I'm not saying this is the "right" way for everyone. If your project requires a very strict spec case then it may not be for you, but in my case this is worlds better than what I had before using mocks. I still think stubbing is a good solution in a few spots so I'm still doing that.