On Thu, Sep 4, 2008 at 4:14 PM, Nick Hoffman <[EMAIL PROTECTED]> wrote: > On 2008-08-27, at 15:25, Mark Wilden wrote: >> >> The other thing I would say is that mocking and stubbing are powerful >> tools that you should add to your arsenal as soon as possible. I've had >> several coworkers who resisted using them, only to finally achieve that >> "aha!" moment later. Your tests get easier to write, and they're less >> brittle to change. > > G'day Mark. I was re-reading this thread and noticed this paragraph of > yours. I've been using RSpec and BDD for about 2 months now, and love it. > > However, I'm not a fan of mocking and stubbing, primarily for two reasons: > 1) I believe that specs should test behaviour, rather than a behaviour's > implementation. > 2) Using mocks and stubs causes your specs and implementation to be tightly > coupled, which often forces you to modify your specs if changes occur in the > implementation. > > However, #2 contradicts what you said about "tests ... [are] less brittle to > change" when using mocks and stubs. Considering that I'm still very new to > mocks and stubs, I'm probably missing something here. When you have a > minute, would you mind countering me?
Hey Nick, I've talked with many people that echo your concern that mocks couple specs to an object's implementation. It's a legitimate concern, of course, however, what people tend to fail to recognize is that at some level, specs are *always* coupled to implementation. Let's consider for a moment what a specification is: a statement describing expected behavior. In programming terms, what an object *does*. Well, what does an object do, anyway? There are three basic things an object can do: * It can respond to a message * It can return a result as the response to a message * It can interact with collaborators Let's look at a super basic spec example and its associated object (just typing it up, please excuse typos): describe BankService, "#debit" do before(:each) do @account = Account.new(100) @service = BankService.new end it "should debit the account" do @service.debit @account, 25 @account.balance.should == 75 end end class BankService def debit(account, amount) account.debit amount end end Now this example is totally contrived - there's nothing going on, there's just a middleman delegating a call to another object. But let's take a look at it anyway. What are the changes that could cause this spec to fail? I can think of several: * BankService.new changes signature * BankService#debit gets renamed or changes signature * Account#debit gets renamed or changes signature * Account.new changes signature * Account.debit changes implementation (e.g. #debit also applies some kind of charge, resulting in #balance returning a different result) That, to me, represents a serious problem. Out of five ways in which this spec could break, only TWO of them are related to the Unit Under Test (and this doesn't include a name/signature change to Account#balance) But what happens if we use a mock object instead? describe BankService, "#debit" do before(:each) do @mock_account = mock("account") @service = BankService.new end it "should debit the account" do @mock_account.should_receive(:debit).with(25) @service.debit @account, 25 end end * BankService.new changes signature (spec fails) * BankService#debit gets renamed or changes signature (spec fails) * Account#debit gets renamed or changes signature (spec still passes) * Account.new changes signature (spec still passes) * Account.debit changes implementation (spec still passes) By using a mock object, we've reduced the number of potential failure causes from 5 to 2. Now, I will grant you that #3 (Account#debit gets renamed or changes signature) may result in a false positive, which is a Bad Thing. It's a false positive in the sense that the *system as a whole* doesn't work though, not that there's something wrong with the BankService object itself. This is why we need integration tests. But basically, as long as the BankService's logic stays correct, the existing specs pass. And this is what happens when there's basically no logic - it's all delegation - so imagine what happens when we have real logic and multiple collaborators! So, any time you write a spec for an object that has one or more collaborators, you must ask yourself the following question: "Do I want my specs to be coupled to this object, or do I want my specs to be coupled to this object's collaborators?" The problem with choosing the second option is that whenever you're coupled to an object's collaborators, you're also coupled to the collaborators' collaborators! When you use mock objects, your specs ensure that you're coupled only to collaborators' interfaces and not to their interfaces AND implementations. That's nice, because collaborators (= dependencies) have their OWN collaborators (=dependencies), so using *real* collaborating objects in specs means that you've introduced a dependency, and all its dependencies, and all its dependencies' dependencies, ad infinitum. So which spec is more brittle in reality? The one that breaks whenever the UUT changes, or the one that breaks whenever one of X nested dependent objects changed? So there we go, a lengthy (but hopefully useful) explanation of why mock objects are good for object-level specs aka unit tests. In short, an interaction-based spec is coupled to its collaborators' interfaces, and a purely state-based spec is coupled to its collaborators' interfaces and implementations, *recursively until there are no more collaborators*. That's that. And there's another piece to this whole "mocks couple your specs to implementation" thing, which is that whenever I notice developers being slowed down by mocks, it's usually because they're missing an abstraction. There's a great paper by Steve Freeman and Nat Pryce called "mock roles not objects" [1] that gets into this. Basically, you'll only have success mocking useful abstractions, not low-level stuff. That is to say, mocking File.read might eliminate your test's dependency on the filesystem, but it doesn't change the fact that your object is dealing with a relatively low-level operation - and ultimately, we care about the design and effectiveness about the production code itself rather than the tests. So instead of mocking out File.read, you spec out a ConfigReader class (for example) that actually reads and parses some file, and once that's complete you use mock objects in any spec that needs a ConfigReader instance. Unfortunately it's late and I've run completely out of steam and can't write anymore / create examples. Bullet points * Your tests are *always* coupled to an implementation at some level * Mocks reduce the number of potential failure causes by eliminating dependencies * Pain when mocking usually points to potential design improvements I encourage you to voice any other comments or concerns you've got, and to point out the holes in my thinking. Pat [1] (PDF) http://www.jmock.org/oopsla2004.pdf _______________________________________________ rspec-users mailing list rspec-users@rubyforge.org http://rubyforge.org/mailman/listinfo/rspec-users