Extra Cheese

A Blog


Test Double Injection Inversion

Jan 21, 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!



Showing 7 comments

Posted by Steve Howell at Tue Jan 19 17:56:47 2010

I prefer this form:

approver = Dingus(approve=returner(True))

Even though I've argued with you at least once before not to take everything Uncle Bob says as gospel, I am with him on his basic premise:

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

But, of course, since his example was in Java, even the "simpler" roll-your-own example was needlessly verbose.

It would be interesting to see the non-Dingus equivalent of your code in Python, just so that we don't conflate the complexity of Java with the complexity of using frameworks.


Posted by Steve Howell at Tue Jan 19 18:21:15 2010

Setting aside the verbosity of Java, I think the lengthiness of Uncle Bob's test code should at least make one pause and consider whether the problem is actually that you are using object injection in BillingService when function injection might work just as well.

What's wrong with this code?

  def approve_method(*args)
  return True

  def log_method(msg)
  assert msg = 'Transaction by Bob approved'

  bs = BillingService(approve_method, log_method)

Pretty terse, and no mocking framework required.  Also, BillingService becomes simpler, since it no longer has to dereference objects needlessly to get to the only methods that it really needs: approve and log.

  class BillingService
  def _init_(self, approve, log):
  self.approve = approve
  self.log = log

  def process_charge(self, amount, person)
  if self.approve(amount, person):
  self.log(...)

That's right, Gary, I am drinking the FP koolaid now.  And who do I have to blame?  YOU!  I saw your functional version of the bowling game. :)


Posted by Amol Jadhav at Tue Jan 19 22:54:24 2010

I like
approver = Dingus(approve__returns=True)
Because, it's much more readable than returner.


Posted by Niki at Wed Jan 20 03:33:17 2010

approver = Dingus(approve=returner(True))
can be
approver = Dingus(approve=const(True))
and i like it


Posted by Mike Bria at Wed Jan 20 08:34:40 2010

I like "approve__returns=True" much better, as it is closer to layman's english than "approve=returner(True)".

Important to note, I'm not a Dingus/Python programmer, so the latter makes me stop and process the geek-speak to map "approve returns true" from "approve=returner(True)", being I have to wonder what a "returner" is.

In the same vane, "approve=const(True)" is even further down the geek-speak tunnel.

Just my $.02.  Cheers.


Posted by Christian Wyglendowski at Wed Jan 20 09:35:01 2010

What about...

approver = Dingus(approve=method.returns(True))

It reads naturally and doesn't do any double-underscore trickery.

Here is some code that implements it:

http://pastebin.com/f7f8fb96b


Posted by Mike Bria at Wed Jan 20 10:46:02 2010

I like "approve__returns=True" much better, as it is closer to layman's english than "approve=returner(True)".

Important to note, I'm not a Dingus/Python programmer, so the latter makes me stop and process the geek-speak to map "approve returns true" from "approve=returner(True)", being I have to wonder what a "returner" is.

In the same vane, "approve=const(True)" is even further down the geek-speak tunnel.

Just my $.02.  Cheers.


Name:


E-mail:


URL:


Comment: