Dependency Injection Inversion 1859
Dependency Injection is all the rage. There are several frameworks that will help you inject dependencies into your system. Some use XML (God help us) to specify those dependencies. Others use simple statements in code. In either case, the goal of these frameworks is to help you create instances without having to resort to new
or Factories.
I think these frameworks are great tools. But I also think you should carefully restrict how and where you use them.
Consider, for example, this simple example using Google’s Guice framework.
public class BillingApplication {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService billingService = injector.getInstance(BillingService.class);
billingService.processCharge(2034, "Bob");
}
}
My goal is to create an instance of BillingService
. To do this, I first get an Injector
from Guice. Then I use the injector
to get an instance of my BillingService
class. What’s so great about this? Well, take a look at the constructor of the BillingService
class.
class BillingService {
private CreditCardProcessor processor;
private TransactionLog transactionLog;
@Inject
BillingService(CreditCardProcessor processor, TransactionLog transactionLog) {
this.processor = processor;
this.transactionLog = transactionLog;
}
public void processCharge(int amount, String id) {
boolean approval = processor.approve(amount, id);
transactionLog.log(
String.format("Transaction by %s for %d %s",
id, amount, approvalCode(approval)));
}
private String approvalCode(boolean approval) {
return approval?"approved":"denied";
}
}
Oh ho! The BillingService
constructor requires two arguments! A CreditCardProcessor
and a TransactionLog
. How was the main
program able to create an instance of BillingService
without those two arguments? That’s the magic of Guice (and of all Dependency Injection frameworks). Guice knows that the BillingService
needs those two arguments, and it knows how to create them. Did you see that funky @Inject
attribute above the constructor? That’s how it got connected into Guice.
And here’s the magic module that tells Guice how to create the arguments for the BillingService
public class BillingModule extends AbstractModule {
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(MyCreditCardProcessor.class);
}
}
Clever these Google-folk! The two bind
functions tell Guice that whenever we need an instance of a TransactionLog
it should use an instance of DatabaseTransactionLog
. Whenever it needs a CreditCardProcessor
it should use an instance of MyCreditCardProcessor
.
Isn’t that cool! Now you don’t have to build factories. You don’t have to use new
. You just tell Guice how to map interfaces to implementations, and which constructors to inject those implementations in to, and then call Injector.getInstance(SomeClass.class);
and voila! You have your instance automatically constructed for you. Cool.
Well, yes it’s cool. On the other hand, consider this code:
public class BillingApplicationNoGuice {
public static void main(String[] args) {
CreditCardProcessor cp = new MyCreditCardProcessor();
TransactionLog tl = new DatabaseTransactionLog();
BillingService bs = new BillingService(cp, tl);
bs.processCharge(9000, "Bob");
}
}
Why is this worse? It seems to me it’s better.
But Uncle Bob, you’ve violated DIP by creating concrete instances!
True, but you have to mention concrete instances somewhere. main
seems like a perfectly good place for that. Indeed, it seems better than hiding the concrete references in BillingModule
.
I don’t want a bunch of secret modules with bind
calls scattered all around my code. I don’t want to have to hunt for the particular bind
call for the Zapple
interface when I’m looking at some module. I want to know where all the instances are created.
But Uncle Bob, You’d know where they are because this is a Guice application.
I don’t want to write a Guice application. Guice is a framework, and I don’t want framework code smeared all through my application. I want to keep frameworks nicely decoupled and at arms-length from the main body of my code. I don’t want to have @Inject
attributes everywhere and bind
calls hidden under rocks.
But Uncle Bob, What if I want to get an instance of BillingService
from deep in the bowels of my application? With Guice I can just say injector.getInstance(BillingService.class);
.
True, but I don’t want to have createInstance
calls scattered all through my code. I don’t want Guice to be poured all over my app. I want my app to be clean, not soaked in Guice.
But Uncle Bob, That means I have to use new
or factories, or pass globals around.
You think the injector
is not a global? You think BillingService.class
is not a global? There will always be globals to deal with. You can’t write systems without them. You just need to manage them nicely.
And, no, I don’t have to use new
everywhere, and I don’t need factories. I can do something as simple as:
public class BillingApplicationNoGuice {
public static void main(String[] args) {
CreditCardProcessor cp = new MyCreditCardProcessor();
TransactionLog tl = new DatabaseTransactionLog();
BillingService.instance = new BillingService(cp, tl);
// Deep in the bowels of my system.
BillingService.instance.processCharge(9000, "Bob");
}
}
But Uncle Bob, what if you want to create many instances of BillingService
rather than just that one singleton?
Then I’d use a factory, like so:
public class BillingApplication {
public static void main(String[] args) {
Injector injector = Guice.createInjector(new BillingModule());
BillingService.factory = new BillingServiceFactory(injector);
// Deep in the bowels of my code.
BillingService billingService = BillingService.factory.make();
billingService.processCharge(2034, "Bob");
}
}
But Uncle Bob, I thought the whole idea was to avoid factories!
Hardly. After all, Guice is just a big factory. But you didn’t let me finish. Did you notice that I passed the Guice injector into the factory? Here’s the factory implementation.
public class BillingServiceFactory extends AbstractModule {
private Injector injector;
public BillingServiceFactory(Injector injector) {
this.injector = injector;
}
protected void configure() {
bind(TransactionLog.class).to(DatabaseTransactionLog.class);
bind(CreditCardProcessor.class).to(MyCreditCardProcessor.class);
}
public BillingService make() {
return injector.getInstance(BillingService.class);
}
}
I like this because now all the Guice is in one well understood place. I don’t have Guice all over my application. Rather, I’ve got factories that contain the Guice. Guicey factories that keep the Guice from being smeared all through my application.
What’s more, if I wanted to replace Guice with some other DI framework, I know exactly what classes would need to change, and how to change them. So I’ve kept Guice uncoupled from my application.
Indeed, using this form allows me to defer using Guice until I think it’s necessary. I can just build the factories the good old GOF way until the need to externalize dependencies emerges.
But Uncle Bob, don’t you think Dependency Injection is a good thing?
Of course I do. Dependency Injection is just a special case of Dependency Inversion. I think Dependency Inversion is so important that I want to invert the dependencies on Guice! I don’t want lots of concrete Guice dependencies scattered through my code.
BTW, did you notice that I was using Dependency Injection even when I wasn’t using Guice at all? This is nice and simple manual dependency injection. Here’s that code again in case you don’t want to look back:
public class BillingApplicationNoGuice {
public static void main(String[] args) {
CreditCardProcessor cp = new MyCreditCardProcessor();
TransactionLog tl = new DatabaseTransactionLog();
BillingService bs = new BillingService(cp, tl);
bs.processCharge(9000, "Bob");
}
}
Dependency Injection doesn’t require a framework; it just requires that you invert your dependencies and then construct and pass your arguments to deeper layers. Consider, for example, that the following test works just fine in all the cases above. It does not rely on Guice, it only relies on the fact that dependencies were inverted and can be injected into BillingService
public class BillingServiceTest {
private LogSpy log;
@Before
public void setup() {
log = new LogSpy();
}
@Test
public void approval() throws Exception {
BillingService bs = new BillingService(new Approver(), log);
bs.processCharge(9000, "Bob");
assertEquals("Transaction by Bob for 9000 approved", log.getLogged());
}
@Test
public void denial() throws Exception {
BillingService bs = new BillingService(new Denier(), log);
bs.processCharge(9000, "Bob");
assertEquals("Transaction by Bob for 9000 denied", log.getLogged());
}
}
class Approver implements CreditCardProcessor {
public boolean approve(int amount, String id) {
return true;
}
}
class Denier implements CreditCardProcessor {
public boolean approve(int amount, String id) {
return false;
}
}
class LogSpy implements TransactionLog {
private String logged;
public void log(String s) {
logged = s;
}
public String getLogged() {
return logged;
}
}
Also notice that I rolled my own Test Doubles (we used to call them mocks, but we’re not allowed to anymore.) It would have been tragic to use a mocking framework for such a simple set of tests.
Most of the time the best kind of Dependency Injection to use, is the manual kind. Externalized dependency injection of the kind that Guice provides is appropriate for those classes that you know will be extension points for your system.
But for classes that aren’t obvious extension points, you will simply know the concrete type you need, and can create it at a relatively high level and inject it down as an interface to the lower levels. If, one day, you find that you need to externalize that dependency, it’ll be easy because you’ve already inverted and injected it.
UI Test Automation Tools are Snake Oil 411
It happens over and over again. I visit a team and I ask about their testing situation. We talk about unit tests, exploratory testing, the works. Then, I ask about automated end-to-end testing and they point at a machine in the corner. That poor machine has an installation of some highly-priced per seat testing tool (or an open source one, it doesn’t matter), and the chair in front of it is empty. We walk over, sweep the dust away from the keyboard, and load up the tool. Then, we glance through their set of test scripts and try to run them. The system falls over a couple of times and then they give me that sheepish grin and say “we tried.” I say, “don’t worry, everyone does.”
It’s a very familiar ‘rabbit hole’ in the industry. It’s sort of like the old days when you’d find a couple of classes generated by a CASE tool in every code base you visited if you looked hard enough. People started with the merry notion that they were going to round-trip code with some CASE tool and they learned (like most lucky teams do) that it just doesn’t pay for itself, it’s not worth the time or the frustration. UI Test Automation tools are in the same category. Personally, I think that in this day and age selling them is irresponsible. Developing them open-source? Well, let your conscience be your guide, but really, even though people can use them responsibly, they hardly ever do because these tools are sold with a dream, a very seductive and dangerous one.
The Dream
Janet comes into work in the morning and she sits down at her super-duper testing console. She presses a button and the testing system springs to life. The application comes up all at once across ten monitors. Cursors move, selections are made (silently) and tests run against the user interface magically, as if some eager set of ghost elves took control, mischievously burrowing through the nooks and crannies of the application, running scripts to completion, and making little notes whenever there is a failure. Janet sits back in her chair, waiting for the elves to report back to her. She stirs her coffee gently.
The Reality
Janet hasn’t gone home yet. It’s 2AM and she has to report completion of all her test cases at a meeting in the morning. She thinks she’s past the last configuration issue but she’s not sure. For the last hour, she’s been trying to make sure that a particular button is pressed at step 14 of her script, but quirky latency on the server is preventing it from happening consistently. Sadly, she has to run the script from the beginning each time. Oh, and five hours ago she discovered UI changes which invalidated 30% of the regression tests. Most of the changes were easy but she still has 12 cases to go and her 9AM meeting looms ahead of her.
This gap between the dream and the reality is not a matter of flawed execution, it’s endemic. Here’s the scoop.
UI Test Automation Tools are Brittle
You might not think this is fair but it is, really. I haven’t seen one of these tools yet which isn’t susceptible to missed events or timing madness. It just happens. The fact of the matter is, it is hard to sit on the outside of an application an instrument it. It’s a very technology sensitive problem. You need to hook into either the OS or the browser or both. Neither are ever really built from the ground up for that sort of access.
UI Based Testing Is Not the Solution That Vendors Imply It Is
This is the big issue, the one which really hurts the industry. The fact of the matter is that UI based testing should be used for UIs: that’s it. You should not be testing your full application end-to-end through a UI Testing tool. First of all, that sort of testing couples some of the most important tests in your system to one of the most volatile parts of it. It’s easy for us to see business logic as volatile, but really, the UI is the thing which twists and ripples in the winds of change. When customers want new features, often those features involve new workflows. When usability experts discover better ways of models of interaction, an agile business seizes upon them and makes the changes—if they can. You’d be surprised at the number of applications which continue to sport out of day user interfaces simply because the development organization is terrified of throwing away all of their regression tests which (by the way) go through the UI. Even if you’re not a consultant like me, visiting teams and seeing their development processes, you can see hints from the outside. Think of every website or shrink-wrap application which has “the same old workflow” and a UI that has become more cluttered over the years. Often it’s because of that lock-in.
UI Based Testing Takes More Staff and Time Than You Expect
This, really, is the most common failure case. It’s the case which explains the dust on the testing box’s keyboard. Someone, usually disconnected from the development organization, decides that “hey, we need to solve the testing problem. We have too many people doing manual testing. It’s taking forever.” So, they do their research, find a vendor with with a good licensing model and a good pitch and then they push it on the development organization. They are, of course, looking to reduce staff so when they realize that translating all of those manual tests to the tool is very labor-intensive, they are taken aback. But, of course, it is just a temporary cost, right? But, then it takes far longer than they expect. Remember Janet’s story? It’s really hard to catch up with a UI-Based testing tool. It’s hard to even stay in place with one. Typically it takes a number of people to do UI-Based automated testing for a development team in sync with an iteration and worse, they’ll always lag behind a bit because you can’t really write UI-based tests ahead of time the way you can with FIT and other beneath-the-UI testing tools. From what I’ve seen UI-based testing, done diligently, takes the effort of about one tester for every two to three developers. That’s what it seems to cost amortized across all of the maintenance of UI-induced test breakage. Oh, and by the way, if think you are going to save labor using record and playback? Nope, you aren’t. It doesn’t work.
Solutions
The fact of the matter is, you can use these tools effectively, but in a very narrow space. It’s nice to be able to test the UI—by itself. However, this sort of thing requires an architectural change.
In general, UIs are too volatile for end-to-end testing. Teams that do it well, typically develop a small task-focused scripting layer and build tests on top if it so that the actual tests don’t touch the UI directly. But, if they happen across that technique, they are lucky. Still, it isn’t an ideal solution. You really want to be below the UI working against an API which exposes the business logic. And, because of that nearly mystical synergy between testability and good design, that API layer is often useful for many things other than testing.
Conclusion/Challenge
I recognize that I’ve been rather vicious in the this blog. If you develop these tools for a living, you might not think it’s fair. But consider this. If you don’t think I’m being fair, take a look at how your tools are marketed. In particular, show me where the product literature discourages end-to-end testing through the tool. Otherwise, well, you know, you are probably developing snake-oil.