Most Android devs know what mocks are and how to use them. They have in fact become an essential part of writing unit tests.
And most Android devs do still also know what spies are: classes that call real methods but can also stub specific methods to return a controlled value. They are otherwise known as partial mocks.
But how many devs have actually used spies in our unit tests? Far far FAR fewer than the number of people who know what spies are.
In the right situation, spies can be very useful for writing effective unit tests. So in this article, we’ll cover what spies are and how to create them, the difference between mocks and spies, and what situations spies can be used to really shine over their mocked counterparts.
Spy Overview
private val spiedList = spy<ArrayList<String>>() val realList = mutableListOf<Int>() val spiedList = spy(realList)
Spies are mocks of classes that call real methods, but can also stub specific methods to return a controlled value. They use real instances of classes and makes them in a way, mockable. In fact, let’s look at the code for the spy method itself.
@Incubating @CheckReturnValue public static <T> T spy(Class<T> classToSpy) { return MOCKITO_CORE.mock(classToSpy, withSettings() .useConstructor() .defaultAnswer(CALLS_REAL_METHODS)); } @CheckReturnValue public static <T> T spy(T object) { return MOCKITO_CORE.mock((Class<T>) object.getClass(), withSettings() .spiedInstance(object) .defaultAnswer(CALLS_REAL_METHODS)); }
A spy is in fact a mock based off of a real object passed into it, or if there’s none, it uses the class’s constructor to make one, provided it has an empty one.
Because it calls real methods, we can have tests like this one.
@Test fun givenString_whenAddToSpiedList_thenSpiedListHasAddedString() { val someString = "helloworld" spiedList.add(someString) assertThat(spiedList.size).isEqualTo(1) }
Because spiedList
calls the real add method of ArrayList
, the size of the list changes and we can assert that.
If we did this with a mock, the test would fail because a mocked list doesn’t call the real add method of ArrayList
. Even if we made this mock call it with willCallRealMethod
, the test would still fail because of internal functions within ArrayList
that are by default, stubs (not real methods).
Creating Spies
Because a spy mimics real behaviour, there’s a few more rules to creating them than there are creating mocks.
To create a spy, the class you’re spying needs to have an empty constructor
val spiedList = spy<ShoppingList>()
Provided ShoppingList here can be created with an empty constructor
You can also create spy by creating a real object and passing that into spy.
val shoppingList = ShoppingList(1, "Sample List") val spiedList = spy(shoppingList)
With spies, real methods are called, but you can stub specific methods to return specific values set in your test.
@Test fun foo() { val realList = mutableListOf<Int>() val spiedList = spy(realList) `when`(spiedList.size).thenReturn(100) spiedList.add(1) spiedList.add(2) println(spiedList) println(spiedList.size) } Output: [1, 2] 100
Forget the utter redundancy of the test, but the code above demonstrates add
essentially functioning as it does in a real mutable list, but because we stubbed size
to return 100, that’s the result we got.
Overuse of spies is a code smell
Ideally in a test, you have a single class you want to test in isolation. If that class has any dependencies, you would create mocks of them so their functionality doesn’t affect the validity of the test.
In fact, looking at the documentation of Mockito.doCallRealMethod
:
Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy objects.
How does partial mock fit into this paradigm? Well, it just doesn’t…
Partial mock usually means that the complexity has been moved to a different method on the same object.
In most cases, this is not the way you want to design your application.
But there are still in fact, some valid use cases of spy.
Uses of Spy
3rd Party Interfaces / Legacy Code
Let’s read further down into the documentation of Mockito.doCallRealMethod
:
However, there are rare cases when partial mocks come handy:
Dealing with code you cannot change easily (3rd party interfaces, interim refactoring of legacy code etc.)
However, I wouldn’t use partial mocks for new, test-driven & well-designed code.
In an ideal world, you wouldn’t have to use spies in our code at all. If you can create effective tests without it, congratulations. Your code is well-designed
However, this isn’t always the case. Legacy code is a fact almost as inevitable as the universe itself.
If you’re working on any sizeable projects, refactoring jobs are by no means a quick process. This is where spies can come in handy
The same can be said for 3rd party interfaces. The point here being, partial mocks are useful for dealing with code that’s you can’t easily change.
Verifying real methods
You may be aware that you can only verify
methods called by mocks, and not by real objects. If you did need to verify a method called by a real object, you can turn it into a spy and perform your test that way.
@Test fun givenListHasItems_whenRemoveFirst_thenVerifyFirstItemRemoved() { val spiedList = spy(mutableListOf(1, 2, 3)) spiedList.removeFirst() verify(spiedList).removeAt(0) }
MutableCollections.removeFirst()
implementation calls removeAt(0)
, and we know that this is expected behaviour so using a spy, we can verify this behaviour.
Sure, for this to work, the method being verified also has to be public, but you get my point.
Stubbing Specific Methods
@Test fun foo() { val realList = mutableListOf<Int>() val spiedList = spy(realList) `when`(spiedList.size).thenReturn(100) spiedList.add(1) spiedList.add(2) println(spiedList) println(spiedList.size) } Output: [1, 2] 100
Real spies allow you to perform real methods, yet still stub and verify specific methods.
Imagine if in your test, out of 7 public methods of a class, you have 6 methods where you want to call the real method, and only 1 method you want to stub. Invoking doCallRealMethod
6 times is effort really. It would be easier to use a spy and stub one out.
Testing abstract classes
There a few ways to test abstract classes. You could test it with a class that’s an implementation of the abstract class, although your test’s validity is affected by the implementation.
You could also use the object: MyAbstractClass
syntax to hard-create an implementation of the abstract class within the test class itself.
A 3rd option is to create a spy of the abstract class.
val abstractClass = spy<MyAbstractClass>()
Do note that this only works for abstract classes with no parameters, due to the way spy works. If it does have any parameters, you’re better with the object
syntax.
Conclusion
Spies shouldn’t be something you need to use when writing new well-designed code, but it can come in handy when testing legacy and other out-of-your-control code.
It’s gonna be one of those things in your testing toolbelt that you’re not gonna be using a lot but it can come in very useful when you do need it.
I hope that shines a bit of light on spies and what purpose they fill. As always, happy coding ༼ つ ◕_◕ ༽つ