Simulating Entire APIs
One of my favorite aspects of MockLib is its ability to simulate an entire API. One of the major problems in simulating an entire API is that it can return a huge object that you need to simulate yet again. It is very important when simulating an entire API to be able to return mock objects from mock objects. The next example will demonstrate this.
This example is a system that receives your emails and when a high-priority email comes in, it calls your cell phone to make sure you check your email ASAP. The EmailToPhone system uses a phone API, simply a PhoneFactory that creates phones (see Figure 4). Not too glamorous but it gets the point across.
As usual, you start by creating the mock objects and the EmailToPhone system. You also make sure you pass the MockFactory into the system so the EmailToPhone system can create phones when it needs them:
38 mockPhoneFactory = MockObjectFactory.createMock(PhoneFactory.class);
39 mockPhone = MockObjectFactory.createMock(Phone.class);
41 sysUnderTest = new EmailToPhoneImpl((PhoneFactory)mockPhoneFactory);
Next, before your test actually calls the receivedHighPriorityEmail method, you must add a return value since createPhone will be called and needs to return a phone. At this point, when writing the test you have decided the implementation may work a certain way. Of course, after you write the implementation you might want to change the test slightly. For now though, assume every time receivedHighPriorityEmail is called, you will create a phone and make a call:
57 //We know that when receivedHighPriorityEmail is called, the EmailToPhone system will
58 //create a Phone so you need to make sure the mock object's passes back a mockPhone
59 //that the test controls
60 mockPhoneFactory.addReturnValue("createPhone", mockPhone);
Next, you have to simulate receiving a high-priority email:
62 String extension = "555-4444";
63 String emailTitle = "xxxx";
64 String emailContents = "yyyy";
65 sysUnderTest.receivedHighPriorityEmail(extension, emailTitle, emailContents);
Lastly, you expect that createPhone was called on the mock PhoneFactory. You then expect the makeCall method to be called on the mock phone that you returned from the mock PhoneFactory. On line 72, you also expect to be calling the right phone number (i.e., verifying makeCall was given the correct extension):
67 //we expect createPhone to be called
70 //we expect a phone call to be made to the correct extension
71 CalledMethod m = mockPhone.expect("makeCall");
72 assertEquals(extension, m.getAllParams());
Again, try to implement the EmailToPhone system yourself. One solution is located solution here.
As you use MockLib more and more, you begin to run into more advanced scenarios. Sometimes, you want to throw an exception the first time a method is called and return a value the second time or vice-versa. MockLib maintains a queue for each method on a class. The methods that add to this queue are:
- addReturnValue queues a return value to be returned
- addThrowsException queues an exception to be thrown
- addBehavior queues a snippet of code to be run when the method is called
All of these methods add to that same queue. So the first time a method is called, a value could be returned; the next time, an exception could be thrown; and finally, a code snippet could be run. They all use the same queue. If this queue happens to be empty, a default value must have been specified. The default can be set with either of these methods:
- setDefaultBehavior sets a default snippet of code to be run when the method is called
- setDefaultReturnValue sets a default return value to be returned
Sometimes, you encounter methods like getId() that you really don't want and would like to ignore. After all, every time you add a new log statement using getId() you would have to change your test case, which is a maintenance nightmare. Instead, tell the MockObject to ignore that method by calling addIgnore("getId");.
MockObject can also deal with multi-threaded systems. By default, it will wait a few seconds for the method it expects to be called (if it has not already been called). There is also a setExpectTimeout to override this default and allow longer times. It can be useful for debugging as well.
Last but not least, MockLib offers a special clone feature. The Behavior class forces you to implement a method that clones the parameters. This allows the test to take a snapshot of what the values of the parameters were when they were passed in. After all, they could change later in the implementation code and your test would fail.
A Power-Packed Six Classes
MockLib offers much more than what this article showed. Even though the API is only six classes, it is still very powerful. Here are some things it enables you to do that weren't covered:
- Tell the MockObject to throw an exception
- Tell the MockObject to return a value
- Run snippets of code when a method is called
- Clone parameters passed into a MockObject
- Retrieve info on how a method was called
You did however learn the differences between testing a listener implementation and mocking a listener. You also saw how to test timer-triggered events in mere nanoseconds. Lastly, you witnessed MockLib's power to simulate an entire API. For more examples on complicated tests, visit these open source projects: