次の方法で共有


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.

Comments

  • Anonymous
    August 14, 2007
    PingBack from http://msdnrss.thecoderblogs.com/2007/08/14/motley-says-if-then-else-hooks-are-the-best-way-to-inject-mock-objects/

  • Anonymous
    August 14, 2007
    TypeMock framework will allow you to inject mock objects without the need to "design to interface"

  • Anonymous
    August 15, 2007
    How about an IoC container like Windsor or StructureMap?

  • Anonymous
    August 16, 2007
    Joe, Could you explain your comment a little more? I am not familiar with Windsor or StructureMap.

  • Anonymous
    August 18, 2007
    "I do wish that people would admit that Dependancy Injection doesn’t have compelling applicability outside of Unit Testing" see this post http://scruffylookingcatherder.com/archive/2007/08/07/dependency-injection.aspx

  • Anonymous
    August 20, 2007
    James, Windsor is an open source IoC container. You can learn more about Windsor here: http://msdn2.microsoft.com/en-us/library/aa973811.aspx Oren, the author of the article, also develops his own mock object framework. You may find it interesting: http://www.ayende.com/projects/rhino-mocks.aspx

  • Anonymous
    August 20, 2007
    Regarding the link Rob brings, I must agree with the responses by Nate Kohari (http://kohari.org/2007/08/15/defending-dependency-injection) and Oren Eini (http://www.ayende.com/Blog/archive/2007/08/18/Dependency-Injection-IAmDonQuixote.aspx). Encapsulating dependencies can be as bad as hard-coding anything else. It looks simpler at first, but it leads to complexity when you need to change something. Using a general purpose DI mechanism can help you realize OCP (Open-Close Principle) in your implementation.

  • Anonymous
    March 13, 2008
    On the sealed issue... Often the reason a class is sealed is that it is exposed outside the package, and therefore could potentially create a security problem.  If this indeed the reason, one can create a wrapper object that is inside the package (package private), that simply delegates to the sealed object, make the dependency from the class under test to the wrapper, and mock the wrapper.                                  package                                  boundary Test --> ClassUnderTest --> Wrapper -|-> SealedClass  |                             ^   -------------------------> Mock

  • Anonymous
    March 13, 2008
    Sorry, my formatting of my "UML" example didn't work when it posted.

  • Anonymous
    March 14, 2008
    Nice suggestion on the wrapper class, Scott. Thanks for the contribution. BTW, I saw the announcement from NetObjectives on your new book. Congratulations! For others, Scott has an interesting new book on emergent design, a favorite topic of mine.

  • Anonymous
    April 28, 2008
    Thanks, James.  So far the book seems to be well-received.  It was sort of a life-goal of mine to "get published", so I guess I can check that one off. :)

  • Anonymous
    April 28, 2008
    Thanks, James.  So far the book seems to be well-received.  It was sort of a life-goal of mine to "get published", so I guess I can check that one off. :)