Mockito - Creating Spies
Creating Mocks was interesting but working Spies is challenging. Apart from the fact that we can stub a spy method, we can also verify its behavior.
Before going into the details regarding how to create a spy, let's first consider what a spy really is. It's an object that can have predefined answers to its method executions, whereas by default it calls the real implementation. It also records the method calls for further verification. So how does it differ from any other test double?
If we are stubbing only some methods from the mock and would prefer that the rest of them execute their real implementations, then it is most likely that the object is doing too much. Consider splitting it into pieces. There are some cases where we would like the real computation to take place by default but we might, however, want to verify whether a particular method was executed.
Create Spies through code
In the coming section, we will see how to create a spy using Mockito code only, without annotations. Again, using Spies usually indicates bad code design. Be very specific in why using them is legitimate in the code.
Getting ready
Our system under test for this sample will be SlabFactorProcessor, which interacts with SlabService.
Let's assume that SlabService does two things.
- It performs calculations of a Slab factor.
- Second it sends a request via a web service to update Slab data for a given user.
If the user's country is not specified explicitly, then a default Slab factor value is returned. In a better code design, one should separate these two concerns (calculation and data update) into separate classes, but for this example, let's leave it as it is. Have a look at the following code:
public class SlabFactorProcessor {
public static final double INVALID_SLAB_FACTOR = -1;
private final SlabService slabService;
public SlabFactorProcessor(SlabService slabService) {
this.slabService = slabService;
}
public double processSlabFactorFor(User user) {
try {
double slabFactor = slabService.calculateslabFactorFor(user);
slabService.updateSlabData(slabFactor, user);
return slabFactor;
} catch (Exception e) {
System.err.printf("Exception [%s] occurred while trying to calculate slab factor for user[%s]%n", e, user.getName());
return INVALID_SLAB_FACTOR;
}
}
}
We will test our system to check that SlabService performs computations but does not call a web service in our unit test.
How it’s done?
To create a spy of a given object using the Mockito API, we need to call the static Mockito.spy(T object) method with the instantiated object for which we want to create a spy.
The following test is written for JUnit. Have a look at the following code:
public class SlabFactorProcessorTest {
SlabService slabService = spy(new SlabService());
SlabFactorProcessorTest systemUnderTest = new SlabFactorProcessor(slabService);
@Test
public void should_return_default_slab_factor_for_person_from_undefined_country() {
// given
doNothing().when(slabService).updateSlabData(anyDouble(), any(Person.class));
// when
double slabFactor = systemUnderTest.processSlabFactorFor(new Person());
// then
then(slabFactor).isEqualTo(SlabService.DEFAULT_SLAB_FACTOR);
}
}
What happens in the test is that we first create a spy for the SlabService instance (via the statically imported Mockito.spy(...) method), and next we create the system under test. In the body of our test, in the //given section, we are stubbing our spy so that it does nothing when the updateSlabData(...) method is called (don't worry if you haven't seen the stubbing syntax of spies before. In the //when section, we are executing the application logic, and in the //then part, we are verifying whether the processed slab factor is the default one from the application.
How it works...
Mockito internally runs the following when you execute the static spy method:
public static <T> T spy(T object) {
return mock((Class<T>) object.getClass(), withSettings()
.spiedInstance(object)
.defaultAnswer(CALLS_REAL_METHODS));
}
Cleary, a spy is in fact a mock that by default calls real methods. Additionally, the MockitoSpy interface is added to that mock.
There are some gotchas regarding spy initialization with Mockito. Mockito creates a shallow copy of the original object so that tested code won't see or use the original object. That's important to know since any interactions on the original object will not get reflected on the spy, and vice versa (if you want to interact directly with the original object, you need to use the AdditionalAnswers.delegateTo(...) answer.
Another issue is final methods. Mockito can't stub final methods, so when you try to stub them, you will not even see a warning message and a real implementation will be called.
Creating spies using annotations
As usual, Mockito offers you the chance to remove a number of lines of code to make your tests more readable and clear. In this recipe, we will remove unnecessary code and convert it into annotations. As a reminder, you should have very legitimate reasons to use a spy in your code. Otherwise, it most likely signifies that there is something wrong with the design of your code.
Getting ready
For this recipe, we will reuse the example from the previous recipe. However, let's take another look at it. Our system under test for this recipe will be a SlabFactorProcessor class that interacts with a SlabService class in order to calculate the slab factor and update the slab data of a given person. Have a look at the following code:
public class SlabFactorProcessor {
public static final double INVALID_SLAB_FACTOR = -1;
private final SlabService slabService;
public SlabFactorProcessor(SlabService slabService) {
this.slabService = slabService;
}
public double processSlabFactorFor(User user) {
try {
double slabFactor = slabService.calculateSlabFactorFor(person);
slabService.updateSlabData(slabFactor, user);
return slabFactor;
} catch (Exception e) {
System.err.printf("Exception [%s] occurred while trying to calculate slab factor for person [%s]%n", e, person.getName());
return INVALID_SLAB_FACTOR;
}
}
}
We will test our system to check that SlabService performs computations but does not call a web service in our unit test.
How to do it...
To profit from Mockito's annotations, we need to perform the following steps:
- For JUnit, annotate the test class with @RunWith(MockitoJUnitRunner.class). For TestNG, copy the necessary TestNG listeners and annotate the test class with @Listeners(MockitoTestNGListener.class).
- We will annotate the object we want to create a spy for with the @Spy annotation.
For both scenarios, we’re using the AssertJ's BDDAssertions.then(...) static method.
Now, let's take a look at the test written for JUnit. Let’s have a look at the following code:
@RunWith(MockitoJUnitRunner.class)
public class SlabFactorProcessorTest {
@Spy SlabService slabService;
@InjectMocks SlabFactorProcessor systemUnderTest;
@Test
public void should_return_default_slab_factor_for_person_from_undefined_country() {
// given
doNothing().when(slabService).updateSlabData(anyDouble(), any(Person.class));
// when
double slabFactor = systemUnderTest.processSlabFactorFor(new Person());
// then
then(slabFactor).isEqualTo(SlabService.DEFAULT_SLAB_FACTOR);
}
}
For TestNG, the test is written as follows:
@Listeners(MockitoTestNGListener.class)
public class SlabFactorProcessorTestNgTest {
@Spy SlabService slabService;
@InjectMocks SlabFactorProcessor systemUnderTest;
@Test
public void should_return_default_slab_factor_for_person_from_undefined_country() {
// given
doNothing().when(slabService).updateSlabData(anyDouble(), any(Person.class));
// when
double slabFactor = systemUnderTest.processSlabFactorFor(new Person());
// then
then(slabFactor).isEqualTo(SlabService.DEFAULT_SLAB_FACTOR);
}
}
How it works...
The creation of spies based on annotations works exactly the same as the mocks presented in the Creating Mocks with annotations.
There's more...
Let's take another look at the @Spy annotated field from our test:
@Spy SlabService slabService
What if we want to create a spy of an object that we want to instantiate in a special way? What if, in our example, SlabService doesn't have a default constructor and we need to provide some explicit value to initialize it?
Before we answer that question, let's check how Mockito works for spy initialization. If we annotate a field with @Spy, Mockito will initialize it if it’s zero argument constructor can be found. The scope of the constructor doesn't need to be public; it can be private too. What Mockito can't do is instantiate local or inner interfaces and classes.
Coming back to the question, how can we create a spy and provide its initialization parameters? We need to explicitly call the object's constructor as follows:
@Spy SlabService slabService = new SlabService("Some value");
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment