Let’s be honest: in practice, dealing with inheritance can be a pain. Functionality that violates “is-a” relationships gets put into base classes for convenience, with applications built around extending these base classes. When superclasses have not been built with inversion of control in mind, unit testing your subclass is difficult.
The Setup
Here’s a not-too-uncommon (from the code I’ve worked on in academics and real work) situation.
- Your class is expected to extend a superclass because that’s how the application is structured.
- The superclass has vital logic that you don’t want to (or can’t – it’s in another .jar) refactor.
- You have not given up the idea of writing unit tests yet. don’t give up!
For clarity, here’s some example code. We have a BaseWorker for handling requests on some queuing system. There is an amount of common infrastructure built around how BaseWorker interacts with the outside world, but basically none of this is important when writing a new worker.
public abstract class BaseWorker {
public void JDBCConnection connection;
protected JDBCConnection getDatabaseHandle() {
return connection;
}
protected User getUserInfo(String username) {
Rows row = connection.executeQuery( ... );
// complicated logic to parse rows and retrieve
// User data
}
public abstract void run(String arg);
}
public class PhotoWorker extends BaseWorker {
public void run(String username) {
User user = this.getUserInfo(username);
List<Photos> photos = user.getPhotos();
// process photos, etc
}
}
Every worker will probably call this.getUserInfo
in the superclass,
which has an external dependency on the database. To write a good
unit test (without pulling our hair out!), we can’t get involved with
this database dependency. The main problem is: how to unit test
PhotoWorker without playing games with the superclass’s external
dependencies?
Test Design One: Anonymous Inner Classes
Until recently, in order to unit test PhotoWorker
, I
would have instantiated it as an anonymous inner class. This
overrides the superclass’s behavior, and if you put a mock in there
you can make your superclass do whatever you want.
public class PhotoWorkerTest {
User mockUser;
@Before
public void setUp() {
mockUser = mock(User.class);
}
public PhotoWorker getPhotoWorker() {
return new PhotoWorker() {
@Override
protected void getUser(String name) {
return mockUser;
}
};
}
@Test
public void processesNoPhotosCorrectly() {
PhotoWorker worker = getPhotoWorker();
when(mockUser.getPhotos()).thenReturn(new LinkedList());
worker.run("bob");
// verify etc
}
}
While this does what I want it to (anonymous inner classes are usually very good at this!), it is not general. If I were writing 10 different workers I would have to write this same override concept for all 10 of them (and declare 10 different User mocks, etc).
Because whatever superclass behavior you want to override when must be
declared when you instantiate that anonymous inner class, there is no
‘clean’ way to write a BaseWorker
test utility that takes
a class and returns one with the proper behavior stubbed out.
Test Design Two: Mockito Spies
One of my favorite new hammers when dealing with unruly superclasses is mockito’s spy concept. This allows you to mock out part of a real class in a clean way (at least in how you specify your tests!)
Just a general note: spies should NOT be used for new code! They should only be used for testing the legacy components of your classes. New code should be written cleanly with all external dependencies declared in the constructor so that this functionality can be controlled with proper mock objects.
public class PhotoWorkerTest {
User mockUser;
@Before
public void setUp() {
mockUser = mock(User.class);
}
public PhotoWorker getPhotoWorker() {
PhotoWorker worker = spy(new PhotoWorker());
doReturn(mockUser).when(worker).getUser(any(String.class));
return worker;
}
@Test
public void processesNoPhotosCorrectly() {
PhotoWorker worker = getPhotoWorker();
when(mockUser.getPhotos()).thenReturn(new LinkedList());
worker.run("bob");
// verify etc
}
}
The syntax is much cleaner here. Based on comments on the mockito documentation, I prefer to write my spy when
clauses backwards: if one form of when
specification works half the time I would just prefer the code to be consistent.
Again however, if we want to set up a number of different BaseWorker
subclass tests, we are out of luck: we will have to write the above doReturn
clause in each one.
Test Design Three: Generic Stubber
Using Java’s generic system, we can write a generic stubber that you can then reuse in all of your other subclass tests. Here’s an example of how this might work.
public class BaseWorkerStubber<T extends BaseWorker> {
T worker;
User user;
public BaseWorkerStubber(T worker) {
this.worker = spy(worker);
this.user = mock(user);
doReturn(user).when(this.worker).getUser(any(String.class));
}
public User getUser() {
return user;
}
public T getWorker() {
return worker;
}
}
public class PhotoWorkerTest {
public BaseWorkerStubber<PhotoWorker> getPhotoWorker() {
return BaseWorkerStubber<PhotoWorker>(new PhotoWorker());
}
@Test
public void processesNoPhotosCorrectly() {
BaseWorkerStubber<PhotoWorker> stubber = getPhotoWorker();
LinkedList<Photo> emptyList = new LinkedList<Photo>();
when(stubber.getUser().getPhotos()).thenReturn(emptyList);
stubber.getWorker().run("bob");
// verify etc
}
}
If you need more specific setup, instead of initializing the when
clauses in the constructor, you can add a setup
function or create static factory methods that do more specific setup.
Conclusion
Mockito spying can be a cleaner way to write unit tests for classes that have their functionality intermingled with their superclass. By taking advantage of Java’s generics system, we can create general test utilities classes that reduce the amount of copy/paste setup needed when writing your unit test.
A Note on Other Languages
For .NET, a cursory Google search indicates that Moq supports spies. The only other Java-specific example used above is the <T extends BaseWorker>
construct, which has a natural analogue in .NET.
I wouldn’t recommend using inheritance in Python at all because of the optional nature of superclass initialization.