Motley says: "If-then-else hooks are the best way to inject mock objects"

Summary

 

Motley:  A simple if-then-else hook is the simplest and best way to inject a mock object.

 

Maven: First, you have to design to interfaces. Then, techniques like dependency injection, factories, and endo-testing are more effective ways to inject mock objects.

______________________________

 

[Context:  Maven and Motley continue a discussion on mock objects. Maven is just about to show Motley how to design his classes to be mockable]

 

Motley: As I said, a simple if-then-else hook in my code is probably the best way to inject a mock object for testing. It's simple, which is one of your key design principles, right?

 

Maven: I'll give you that - it's simple on the outside, but what about maintaining that code into the future? If-then-else hooks all over the place end up looking like spaghetti when you add more over time. Plus, there are other disadvantages.

 

Motley: First you tell me simple rules, and then you tell me simple leads to spaghetti. You know, Mave, sometimes I want to give you a shot in the-

 

Maven: Whoa! Stow your temper for a bit, bud. Simplicity, by default, is good. However, there are other slightly m ore elegant solutions that are still simple to follow and maintain. The "if" hooks, although simple to start, lead to complexity. They also require us couple test code to our production code if we inline the "if" statements. Both of those are far from optimal.

 

Motley: Ok, let's hear about your so-called "elegant" solutions.

 

Maven: Let's do it! Each require a little bit of object-oriented knowledge, but you're an expert, so no problem.

 

Motley: Ah, you flatter me. You're also right.

 

Maven: Above everything else, as we discussed in the past, you need to design to interfaces. Once you have well defined interfaces between your component boundaries, you can plug and play different implementations - mock objects are a great example. But, that's not all. You need to employ a simple technique to invoke the mock objects when required. The first technique is known as "dependency injection."

 

Motley: Sounds like something a drug addict would do!

 

Maven: Funny guy. Anyway, let's say we are developing an account class for a bank, and that we are doing some kind of transfer service from one account at our bank to some other remote bank over a secure web service. Perhaps we have code like this:

 

public class Account

{

public void Transfer(Money amount)

{

remoteBank.Transfer(amount);

}

}

 

Pretty straight-forward. Assume that remoteBank talks to a web service somewhere over the wire. We discussed that good unit tests cannot access the network - they have to remain isolated and fast. So, we need to replace the remote bank with something else. We could take your way of putting in an "if" hook, but let's use dependency injection. The remote bank needs a well-defined interface. Assuming that exists, here's what we do:

 

public class Account

{

public void Transfer(IRemoteBank bank, Money amount)

{

bank.Transfer(amount);

}

}

 

Motley: Great. The code basically looks like the same. Are you sniffing something?

 

Maven: Ah, it may look similar, but it's not the same. A client of the Account class can now pass in an implementation of the remote bank that implements the IRemoteBank interface. That implementation can be a real remote bank (probably by default) or a mock.

 

Motley: I see some advantages, but jeez, now clients have to know about remote banks, which is arguably an implementation detail of the Account class.

 

Maven: Very astute, my friend! Definitely a disadvantage mixed in with the advantages. Another technique is to use the factory pattern to return us the right implementation for our needs. Check this out:

 

public class Account

{

public void Transfer(Money amount)

{

IRemoteBank bank = BankFactory.CreateRemoteBank();

bank.Transfer(amount);

}

}

 

public class BankFactory

{

static public IRemoteBank CreateRemoteBank()

{

if (someCondition)

{

return new MockRemoteBank();

}

else

{

return new RemoteBank();

}

}

}

 

The "someCondition" in this case could be a flag or a setting from a configuration file that is true when tests are executing.

 

Motley: Nice, to some extent. We've now made it such that clients don't need to know anything about implementation detail, but you still have your test code coupled to your core business logic. Ideally we'd like to get rid of that.

 

Maven: I agree! Factories are nice to separate creation from usage, but they don't solve all our problems. This next technique, called Endo-testing, takes care of some of those disadvantages. Here's the code:

 

public class Account

{

public void Transfer(Money amount)

{

IRemoteBank bank = this.createRemoteBank();

}

 

protected virtual IRemoteBank createRemoteBank()

{

return new RemoteBank();

}

}

 

Motley: Big deal. So you have an extra helper method to create a new instance of the remote bank object. What does that solve?

 

Maven: Ah, but notice the virtual on the method declaration. That means that I can subclass the account object, override the createRemoteBank() method on the subclass, and return a mock object for test purposes.

 

Motley: Oh, um, I was just testing you. You can definitely override that method for test purposes. I guess that has the advantage that our test code is completely decoupled from our product code and clients still don't need to know about the implementation details.

 

Maven: You bet! There is a disadvantage though-

 

Motley: Yeah! You can't seal your classes!

 

Maven: Yes. We have to allow subclassing from our class even though we may not want to. And, we have a "protected" method, which for documentation purposes is essentially public. We really should document it.

 

Motley: Those seem like small disadvantages though relative to the alternatives. It's a bit more complicated, though, because you have to understand polymorphism, but a good OO developer can deal with that.

______________________________

 

Maven's Pointer: Know any more good techniques for injecting mock objects? Let us know with a comment below. Of course, all this requires that you design to interfaces, which was one of the main design principles discussed previously.

 

Maven's Resources: 

  • Working Effectively with Legacy Code, by Michael Feathers, Prentice Hall PTR, ISBN: 0131177052, Sept. 2004.