A blog by Gary Bernhardt, Creator & Destroyer of Software

Test Double Injection Inversion

19 Jan 2010

In Dependency Injection Inversion, Uncle Bob wonderfully explains the difference between Dependency Injection and Dependency Injection Frameworks, a topic I've done a lot of thinking about recently. You should go read his post right now if you haven't yet.

At the end, he provides the test code below as an example of testing some dependency-injected Java code:

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;
  }
}

It's perfectly fine Java code, and it wonderfully demonstrates the power of injection. After the code, Uncle bob says:

It would have been tragic to use a mocking framework for such a simple set of tests.

In Java, I agree completely. In a more modern language, I disagree completely! I've translated his example to Python using my Dingus test double library to illustrate the simplicity that doubles can provide:

class BillingServiceTest:
    def setup(self):
        self.log = Dingus()

    def test_approval(self):
        approver = Dingus(approve__returns=True)
        bs = BillingService(approver, self.log)
        bs.process_charge(9000, 'Bob')
        assert self.log.calls(
            'log',
            'Transaction by Bob for 9000 approved').once()

    def test_denial(self):
        denier = Dingus(approve__returns=False)
        bs = BillingService(approver, self.log)
        bs.process_charge(9000, 'Bob')
        assert self.log.calls(
            'log',
            'Transaction by Bob for 9000 denied').once()

In a real system, I'd factor these tests slightly differently; I've left them as close to Bob's as possible. This is 13 ELOC vs. Bob's 38 – only about a third as much code! Some of the difference is in his testing library's ceremony, but most of it is in his test doubles. For example, he says:

class Approver implements CreditCardProcessor {
  public boolean approve(int amount, String id) {
    return true;
  }
}

That is a lot of code! All it really says is "the approve method always returns true", with the rest being a complex dance around Java's rigidity. This is a liability for programmers working in such languages, as well as a learning barrier for new testers. In my Python version, the following takes the place of the Approver class, as well as its instantiation:

approver = Dingus(approve__returns=True)

That line of code is so close to "the approve method always returns true" that I can't imagine it being any clearer. Of course, if the magic double underscores turn you off, you can also say:

approver = Dingus(approve=returner(True))

Digression

I'd love to hear what you think about those two alternate forms. I want to deprecate one, but I don't know which.

I fear that statements like Uncle Bob's about test doubles may lead newer programmers, and static-only programmers, astray. His advice is wonderful, but only in certain domains. Like so many things in software, doubling is far easier when the shackles of Javaesque type systems are removed. And, if you worry that the complexity is simply moved into the test double library, fear not: Dingus is currently 193 ELOC long, including plenty of features not mentioned here!