Unit Testing Mock Basics
Here is a really obvious form of unit testing. Trying all sorts of wacky stuff on one method that doesn't depend on
other methods.
public void testLongitude()
{
assertEquals( "-111.44" , Normalize.longitude( "111.44w" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44W" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44 w" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44 W" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44 w" ) );
assertEquals( "-111.44" , Normalize.longitude( "-111.44w" ) );
assertEquals( "-111.44" , Normalize.longitude( "-111.44W" ) );
assertEquals( "-111.44" , Normalize.longitude( "-111.44 w" ) );
assertEquals( "-111.44" , Normalize.longitude( "-111.44 W" ) );
assertEquals( "-111.44" , Normalize.longitude( "-111.44" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44-" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44 -" ) );
assertEquals( "-111.44" , Normalize.longitude( "111.44west" ) );
// ...
}
Sure, any putz can unit test that sort of thing. But most business logic uses other business logic:
public class FarmServlet extends ActionServlet
{
public void doAction( ServletData servletData ) throws Exception
{
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) )
{
FarmEJBRemote remote = FarmEJBUtil.getHome().create();
remote.addAnimal( species , buildingID );
}
}
}
Not only is this calling other business logic, it's calling an application server! Possibly across a network!
Thousands of these are gonna take more than ten seconds. Plus, changes in the EJB stuff could break my tests here!
So a mock object needs to be introduced.
If I could just mock out all of the EJB stuff, I'd be sitting pretty. Hmmmm .... If the code were to somehow
get my mock FarmEJBRemote, I'd be in fat city.
First, to create the mock. If FarmEJBRemote were a class, I would extend it and override all the methods. But since
it happens to be an interface, I'll just make a fresh class and implement all the methods:
public class MockRemote implements FarmEJBRemote
{
String addAnimal_species = null;
String addAnimal_buildingID = null;
int addAnimal_calls = 0;
public void addAnimal( String species , String buildingID )
{
addAnimal_species = species ;
addAnimal_buildingID = buildingID ;
addAnimal_calls++;
}
}
The mock is dumb. Really dumb. It just carries data between my unit test and the code I am trying to exercise.
Does this class make you .... uncomfortable? It should. There are two things about this sort of class that bugged
me the first time I was exposed to it: The class attributes are not private, and they have underscores in them. The first
time I saw a mock object like this
I was told "Your unit test code doesn't go into production, so it can cut a few corners." I dunno ... I want to only write first class
code all the time! Not even an hour had passed and I needed to mock java.sql.Connection. 40 methods! Getters and setters for every
parameter, return value and counter for every method? .... hmmmm .... thinking this through a little ....
the reason we make the attributes private is for the sake of encapsulation - to hide how things are done on the
inside so that we can change our business logic later without breaking a bunch of stuff that decided to tap into our
innards. But that doesn't really apply to a mock, does it? By definition, a mock has zero business logic. Further,
it doesn't really have anything that it didn't just copy from somebody else. All mock objects everywhere could
easily be 100% generated at build time! ... So I sometimes still feel a little queasy about this, but in the end
I always end up re-convincing myself that this is the best way. So it is still "first class code all the time" - it just
smells a little off. But it smells better than if I did it the other way.
Now I need to get the code to take in my mock object instead of firing up some application server. So here's that snippet
of code again and I've highlighted the line of code where I want to use my mock.
public class FarmServlet extends ActionServlet
{
public void doAction( ServletData servletData ) throws Exception
{
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) )
{
FarmEJBRemote remote = FarmEJBUtil.getHome().create();
remote.addAnimal( species , buildingID );
}
}
}
First, let's separate that out a bit from the rest of the herd ...
public class FarmServlet extends ActionServlet
{
private FarmEJBRemote getRemote()
{
return FarmEJBUtil.getHome().create();
}
public void doAction( ServletData servletData ) throws Exception
{
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) )
{
FarmEJBRemote remote = getRemote();
remote.addAnimal( species , buildingID );
}
}
}
This is gonna hurt a little .... I will now extend my production class and override getRemote() so I can foist my
mock into this operation. So I'll need to make one little change ...
public class FarmServlet extends ActionServlet
{
FarmEJBRemote getRemote()
{
return FarmEJBUtil.getHome().create();
}
public void doAction( ServletData servletData ) throws Exception
{
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) )
{
FarmEJBRemote remote = getRemote();
remote.addAnimal( species , buildingID );
}
}
}
If you are a good OO engineer, you should be hopping mad right about now! Oh sure, violating encapsulation in unit
test code is mighty uncomfortable, but violating encapsulation in production code JUST AIN'T DONE! (in case you missed it,
I took out the keyword "private", making the method "package private" - now, anything in the same package can
see that method) Again, a long winded explanation might help smooth things over a bit. I'm going to save that
for the forums and say for now: be ever vigilant about first class encapsulation in your production code ...
but ... once in a while ... you might consider trading a dollar's worth of encapsulation for twenty dollars worth of
testability. And to salve your pain/shame a bit, you might add a comment:
public class FarmServlet extends ActionServlet
{
//exposed for unit testing purposes only!
FarmEJBRemote getRemote()
{
return FarmEJBUtil.getHome().create();
}
public void doAction( ServletData servletData ) throws Exception
{
String species = servletData.getParameter("species");
String buildingID = servletData.getParameter("buildingID");
if ( Str.usable( species ) && Str.usable( buildingID ) )
{
FarmEJBRemote remote = getRemote();
remote.addAnimal( species , buildingID );
}
}
}
Now I just write the class so I can return the mock:
class FarmServletShunt extends FarmServlet
{
FarmEJBRemote getRemote_return = null;
FarmEJBRemote getRemote()
{
return getRemote_return;
}
}
Note the weird name: "Shunt". I'm not certain, but I think this word comes from electrical engineering/tinkering
and refers to using a wire to temporarily complete a circuit. At first it sounded really stupid to me, but after a while
I got used to it.
A shunt is kinda like a mock, except you don't override all of the methods. This way, you mock some methods
while testing others. A unit test could end up with several shunts all overriding the same class, each testing different
parts of the class. Shunts are usually inner classes.
Now for the grand finale! The actual unit test code!
public class TestFarmServlet extends TestCase
{
static class FarmServletShunt extends FarmServlet
{
FarmEJBRemote getRemote_return = null;
FarmEJBRemote getRemote()
{
return getRemote_return;
}
}
public void testAddAnimal() throws Exception
{
MockRemote mockRemote = new MockRemote();
FarmServletShunt shunt = new FarmServletShunt();
shunt.getRemote_return = mockRemote();
// just another mock to make
MockServletData mockServletData = new MockServletData();
mockServletData.getParameter_returns.put("species","dog");
mockServletData.getParameter_returns.put("buildingID","27");
shunt.doAction( mockServletData );
assertEquals( 1 , mockRemote.addAnimal_calls );
assertEquals( "dog" , mockRemote.addAnimal_species );
assertEquals( 27 , mockRemote.addAnimal_buildingID );
}
}
Now that the basic framework is in place, I just need to add lots of assertions.
TestFarmServlet vs. FarmServletTest: You gotta join one camp or the other. The folks from the latter camp make a
damn good point: "FarmServletTest" sounds more like a noun and thus more OO. I am in the former camp.
I have become addicted to my IDE and enjoy the way it can complete class names
for me. When I get to the point that I have a rich suite of tests, and my test class names all end with "Test",
then my IDE makes twice as many suggestions as I want it to. When my test class names all start with "Test",
my IDE makes exactly the right number of suggestions.
Comments? Questions? Rude gestures? Click here!
|