草庐IT

ruby-on-rails - 测试 : how to focus on behavior instead of implementation without losing speed?

coder 2025-04-13 原文

似乎有两种完全不同的测试方法,我想引用它们。

问题是,这些意见是在 5 年前(2007 年)提出的,我很感兴趣,从那以后发生了什么变化,我应该走哪条路。

Brandon Keepers :

The theory is that tests are supposed to be agnostic of the implementation. This leads to less brittle tests and actually tests the outcome (or behavior).

With RSpec, I feel like the common approach of completely mocking your models to test your controllers ends up forcing you to look too much into the implementation of your controller.

This by itself is not too bad, but the problem is that it peers too much into the controller to dictate how the model is used. Why does it matter if my controller calls Thing.new? What if my controller decides to take the Thing.create! and rescue route? What if my model has a special initializer method, like Thing.build_with_foo? My spec for behavior should not fail if I change the implementation.

This problem gets even worse when you have nested resources and are creating multiple models per controller. Some of my setup methods end up being 15 or more lines long and VERY fragile.

RSpec’s intention is to completely isolate your controller logic from your models, which sounds good in theory, but almost runs against the grain for an integrated stack like Rails. Especially if you practice the skinny controller/fat model discipline, the amount of logic in the controller becomes very small, and the setup becomes huge.

So what’s a BDD-wannabe to do? Taking a step back, the behavior that I really want to test is not that my controller calls Thing.new, but that given parameters X, it creates a new thing and redirects to it.

大卫·切利姆斯基:

It’s all about trade-offs.

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.

Ryan Bates :

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.

最佳答案

我认为这三种观点还是完全成立的。 Ryan 和我一直在为模拟的可维护性而苦苦挣扎,而 David 认为为了提高速度而进行维护权衡是值得的。

但这些权衡是更深层次问题的征兆,David 在 2007 年提到了这个问题:ActiveRecord。 ActiveRecord 的设计鼓励你创建做太多事情的神对象,对系统的其余部分了解太多,并且有太多的表面积。这会导致测试需要测试的内容太多,对系统的其余部分了解得太多,而且要么太慢要么太脆弱。

那么解决方案是什么?尽可能多地将应用程序与框架分开。编写许多模拟您的领域的小类,并且不继承任何东西。每个对象都应该有有限的表面积(不超过几个方法)和通过构造函数传入的显式依赖项。

通过这种方法,我只编写了两种类型的测试:独立的单元测试和全栈系统测试。在隔离测试中,我模拟或 stub 所有不是被测对象的东西。这些测试非常快,通常甚至不需要加载整个 Rails 环境。全栈测试锻炼了整个系统。他们速度慢得令人痛苦,并且在失败时给出无用的反馈。我写的尽可能少,但足以让我相信我所有经过良好测试的对象都能很好地集成。

不幸的是,我无法向您指出一个(目前)做得很好的示例项目。我在关于 Why Our Code Smells 的演讲中谈到了一点,观看 Corey Haines 在 Fast Rails Tests 上的演讲,我强烈推荐阅读 Growing Object Oriented Software Guided by Tests .

关于ruby-on-rails - 测试 : how to focus on behavior instead of implementation without losing speed?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11006888/

有关ruby-on-rails - 测试 : how to focus on behavior instead of implementation without losing speed?的更多相关文章

随机推荐