Testing code that makes RPC calls is especially discouraging. You'll say "I can't unit test it, it needs to set up an RPC server and that's too complicated for JUnit", or, if you're like me, you won't even make up an excuse.
Of course, this laziness comes back to bite you when the code goes into production, the RPC server throws a one-in-a-million exception, and your entire service bites the dust because you never tested that execution path.
So, given that you don't like to be woken up at 3AM by sysops when you have been out drinking all night, let's unit test our RPC clients. Let's do it without having to start up an RPC server when the test runs, and it would be nice to be able to have fine-grained control over the RPC methods.
She's Thrifty - She's Just My Type
This is the Thrift RPC definition we will be using for this example:
service MyRPCService { i64 getDocidForUrl(1: string url), }Simple. We'll be looking up a 64-bit integral document identifier for a given URL. Our client code will make a decision about the state of the document given that identifier.
This is the class we will be testing:
public class ProgramToTest { // class constants private static final int RPC_SERVER_PORT = 3141; private static final String RPC_SERVER_HOST = "rpcserver.teddziuba.com"; private static final long DOCID_IS_OLD_IF_LESS_THAN = 1000; public static enum DocumentStatus { OLD, NEW, UNKNOWN }; // instance variables private MyRPCService.Iface myRpc; private TSocket socket; public ProgramToTest() {} private void init() throws TTransportException { socket = new TSocket(RPC_SERVER_HOST, RPC_SERVER_PORT); TProtocol protocol = new TBinaryProtocol(socket, true, true); myRpc = new MyRPCService.Client(protocol); socket.open(); } public EnumIf I were still in CS class in college, I would get dinged for having multiplegetDocumentStatus(String documentUrl) { try { long docId = myRpc.getDocidForUrl(documentUrl); if (docId < DOCID_IS_OLD_IF_LESS_THAN) { return DocumentStatus.OLD; } return DocumentStatus.NEW; } catch (TException e) { return DocumentStatus.UNKNOWN; } } public void finished() { socket.close(); } }
return
statements, but the best part about being a grown up is that when I want a cookie, I can have a cookie.The
getDocumentStatus
is really the only thing we need to test, as clients of this class will be responsible for dealing with a TTransportException
if the socket initialization fails. The unfortunate part about testing that method is that it makes an RPC call. Sockets. Exceptions. Icky. Even though it's easier to say screw it and go have a beer, remember: you gotta do what you gotta do.Making a Mockery
JMock is a clever unit testing library that makes mock objects really easy. If you're new to mock objects, read more about them here. The basic idea is that we will make an object that "mocks" the behavior of the RPC server, but without doing any I/O. That way, we have complete control over the operations of the server, and can actually test how your client code interacts with that one-in-a-million exception.
We'll be mocking out the
MyRPCService.Iface
interface that is autogenerated by Thrift, and defining our own behavior for it. If you've got some experience with JMock, this should be pretty straight forward, and if not, then you'll catch on quick. JMock's syntax focuses on making the testing conditions human readable.Prepare The Class For Testing
Since we will be providing the
ProgramToTest
class with a mocked version of this interface, we need to add a constructor to the class for testing only:public ProgramToTest(MyRPCService.Iface testOnlyIface) { this.myRpc = testOnlyIface; }JUnit. We In It.
We'll test the low-hanging fruit first. Using our mock to control the return value of the RPC call, we can make sure the logic works:
@Test public void testHandlesOldDocid() throws TException { final MyRPCService.Iface mockedRpc = context.mock(MyRPCService.Iface.class); ProgramToTest testObject = new ProgramToTest(mockedRpc); final long rpcCallReturnValue = 100L; final String testUrl = "http://www.teddziuba.com/"; context.checking(new Expectations() { { one(mockedRpc).getDocidForUrl(with(equal(testUrl))); will(returnValue(rpcCallReturnValue)); } }); assertEquals(ProgramToTest.DocumentStatus.OLD, testObject.getDocumentStatus(testUrl)); }That is pretty cool. Without a whole lot of effort, we've managed to make a unit test for a method that depends on an RPC server. This test does not require any network I/O and runs very quickly. It can be run in a self-contained environment, like an automated test server. I call this kind of test hermetic, because nothing outside of the test code can affect its outcome.
We can also use JMock to test what happens when an exception is thrown. If a Thrift RPC server throws an exception somewhere in its handler method and that exception is not caught server-side, it will be thrown up to the client as a
TException
. To simulate this, we simply change one line of the test expectations:@Test public void testHandlesException() throws TException { final MyRPCService.Iface mockedRpc = context.mock(MyRPCService.Iface.class); ProgramToTest testObject = new ProgramToTest(mockedRpc); final TException rpcException = new TException("something awful has happened."); final String testUrl = "http://www.teddziuba.com/"; context.checking(new Expectations() { { one(mockedRpc).getDocidForUrl(with(equal(testUrl))); will(throwException(rpcException)); } }); assertEquals(ProgramToTest.DocumentStatus.UNKNOWN, testObject.getDocumentStatus(testUrl)); }Go And Do Likewise
JMock is an incredibly useful library. If you're a lazy tester like me, it beats the pants off of subclassing. Now, you have no excuse for leaving RPC calls untested.