YaK:: WebLog #535 Topic : 2006-06-28 20.46.31 matt : How TDD related to Inversion of Control and Dependency Injection [Changes]   [Calendar]   [Search]   [Index]   [PhotoTags]   
  [Back to weblog: pretention]  
[mega_changes]
[photos]

How TDD related to Inversion of Control and Dependency Injection

In a private email discussion, a friend asked about my thoughts on how TDD and IOC/DI relate. Here's that email, edited, with additional commentary.


My friend Dan Moniz (http://pobox.com/~dnm/) emailed me the other day to include me in a thread with another person about TDD-related things. He has dabbled in XP/agile/TDD things, and we've had good discussions in the past. Dan is one of the only people where the TDD discussion can veer into JIT optimizations, CPU architectures, and back with no bullshit and without skipping a beat. He's real fun :)

from Dan Moniz:

Also, following up on our chat about IOC/DI (Inversion of Control/Dependency Injection), one seemingly applicable and compelling reason to use it that I recently stumbled on was in regards to unit testing and TDD in general:

"Dependency injection is a way to achieve loose coupling. The technique results in highly testable objects, particularly when applying test-driven development using mock objects: Avoiding dependencies on the implementations of collaborating classes (by depending only on interfaces that those classes adhere to) makes it possible to produce controlled unit tests that focus on exercising the behavior of, and only of, the class under test. To achieve this, dependency injection is used to cause instances of the class under test to interact with mock collaborating objects, whereas, in production, dependency injection is used to set up associations with bona fide collaborating objects."

I also breezed through a skim of Fowler's article on DI, and he basically nets out saying that DI is probably better for complex pps where classes will be used in multiple applications, i.e. enterprise apps like the kind he works on the most. Otherwise, he says Service Locator is probably a better pattern. I haven't read it in detail yet though.

Finally, in blog post of Fowler's, he mentions Domain Driven Design and I recall that you had that book and liked it, and I've occasionally thought about picking it up when in a bookstore, but still didn't know enough about it to grab it. What's your take?

Matt replies (and then some):

I've never read his book on Domain Driven Design. I was probably talking about Refactoring or Patterns of Enterprise Application Architecture . (By the way, I also recommend Refactoring to Patterns .)

If you are doing TDD, you get this for free automatically. That is, doing real unit-level tests up-front requires and encourages this kind of capability (loose coupling) in the first place. People who don't understand this end up writing system-level tests crammed into a unit test framework -- not the worst thing ever, but the tests are usually very slow and difficult to debug when they fail. A big clue is when you end up having to single-step in the debugger to figure out why your test failed -- when that happens, your objects and methods aren't allowing you to think in a localized enough fashion. (We made this mistake initially with BugScan before making our own automated system test framework -- a 20 line shell script that allowed us to get our unit test run of ~1,000 tests down to seconds and feel really good about piling on more system tests.) Contributing to this is people who don't understand the appropriate use of Dynamic Mocks (aka Mocks) or Static Mocks (aka Fakes), which are critical to this approach.

One *shouldn't* care about how the collaberating objects are *implemented*, just so long as the *interfaces* do what you need. Does this sound familiar? It should -- it's one of the primary design principles expressed in the GoF Design Patterns book .

You can do something like Service Locator, which is kind of a registry for singletons, if that makes sense in the context you're talking about. Keep in mind that this sets up a global state that your test then has to not only set up correctly, but tear down correctly -- lest is screw up global state other tests will then be affected by.

This is something I'm pretty torn by -- I always question the need for a global instance or a static class: why is access to this object so widespread and not localized and why can't it explicitly be passed into a dependency via a parameter to a ctor/method? If it has SetUp/TearDown-like methods called only from one object whose sole responsibility is a faciliator, why isn't that object also managing the creation of those objects and exposing them via? After all, if it's effectively holding data then there is no need to test it and the faciliator can be the singleton. Then again, sometimes there is no good answer for these questions and a global-like construct makes sense -- that's okay. Just really ask those questions first, evaluate if the code isn't telling you something about your design, and if there isn't a non-global refactoring/pattern that can be applied instead.

In one recent case I saw, global instances coupled with a tendency toward a procedural "design" yielded a rat's nest of circular object dependencies inside of circular object dependencies. All the dependencies were well-hidden because they weren't explicitly part of any interface. Also, because the global instances violated a primary rule of Singletons and kept state, this meant that debugging was incredibly difficult for all the reasons that rampant global state is bad. It also made it difficult to get the tests passing consistently, because it only took one test tweaking the global state for many other tests to fail.

Back to Dependency Injection and a security-related note, this allows for negative testing -- emulating database, socket, disk, parameter validation, and other errors. I usually start with negative tests when I'm starting, in fact. This is because I usually try to first think of how things will break or what a method will reject: null parameters, empty strings, improperly bounded values, etc. It's easy to write [ExpectedException()] tests in NUnit, easy to make them pass (usually), and it allows me focus on the error-handling aspects of the interface.

A high quality, loosely coupled interface with robust objects and tests that ensure that robustness means moving much more quickly in feature adds, bug fixing, and optimization. When a project gets into that mode of "no fear" relating to the code, it is Pure Joy.

Discussion:

showing all 0 messages    

(No messages)

>
Post a new message:

   

(unless otherwise marked) Copyright 2002-2014 YakPeople. All rights reserved.
(last modified 2006-06-28)       [Login]
(No back references.)