Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TDD and DI: dependency injections becoming cumbersome

C#, nUnit, and Rhino Mocks, if that turns out to be applicable.

My quest with TDD continues as I attempt to wrap tests around a complicated function. Let's say I'm coding a form that, when saved, has to also save dependent objects within the form...answers to form questions, attachments if available, and "log" entries (such as "blahblah updated the form." or "blahblah attached a file."). This save function also fires off emails to various people depending on how the state of the form changed during the save function.

This means in order to fully test out the form's save function with all of its dependencies, I have to inject five or six data providers to test out this one function and make sure everything fired off in the right way and order. This is cumbersome when writing the multiple chained constructors for the form object to insert the mocked providers. I think I'm missing something, either in the way of refactoring or simply a better way to set the mocked data providers.

Should I further study refactoring methods to see how this function can be simplified? How's the observer pattern sound, so that the dependent objects detect when the parent form is saved and handle themselves? I know that people say to split out the function so it can be tested...meaning I test out the individual save functions of each dependent object, but not the save function of the form itself, which dictates how each should save themselves in the first place?

like image 955
Chris Avatar asked Mar 03 '09 15:03

Chris


People also ask

What's a common type of dependency injection used during TDD?

The simplest form to understand what dependency injection is to think of a setter method. A setter method will take one parameter and set a class variable from this parameter. This is using code injection to pass in a parameter to be used as the class variable value.

What's the difference between the dependency injection and service locator patterns?

The key difference is that with a Service Locator every user of a service has a dependency to the locator. The locator can hide dependencies to other implementations, but you do need to see the locator. So the decision between locator and injector depends on whether that dependency is a problem.

What is dependency injection DI )? What are the types of DI?

A class is no longer responsible for creating the objects it requires, and it does not have to delegate instantiation to a factory object as in the Abstract Factory design pattern. There are three types of dependency injection — constructor injection, method injection, and property injection.

Why do we need dependency injection?

Dependency injection is a technique used in software engineering to develop loosely coupled systems. It is a technique that allows an object to receive other objects that it depends on by some other program than it explicitly calling the dependent object.


1 Answers

First, if you are following TDD, then you don't wrap tests around a complicated function. You wrap the function around your tests. Actually, even that's not right. You interweave your tests and functions, writing both at almost exactly the same time, with the tests just a little ahead of the functions. See The Three Laws of TDD.

When you follow these three laws, and are diligent about refactoring, then you never wind up with "a complicated function". Rather you wind up with many, tested, simple functions.

Now, on to your point. If you already have "a complicated function" and you want to wrap tests around it then you should:

  1. Add your mocks explicitly, instead of through DI. (e.g. something horrible like a 'test' flag and an 'if' statement that selects the mocks instead of the real objects).
  2. Write a few tests in order to cover the basic operation of the component.
  3. Refactor mercilessly, breaking up the complicated function into many little simple functions, while running your cobbled together tests as often as possible.
  4. Push the 'test' flag as high as possible. As you refactor, pass your data sources down to the small simple functions. Don't let the 'test' flag infect any but the topmost function.
  5. Rewrite tests. As you refactor, rewrite as many tests as possible to call the simple little functions instead of the big top-level function. You can pass your mocks into the simple functions from your tests.
  6. Get rid of the 'test' flag and determine how much DI you really need. Since you have tests written at the lower levels that can insert mocks through areguments, you probably don't need to mock out many data sources at the top level anymore.

If, after all this, the DI is still cumbersome, then think about injecting a single object that holds references to all your data sources. It's always easier to inject one thing rather than many.

like image 67
Uncle Bob Avatar answered Nov 28 '22 10:11

Uncle Bob