Stubbing Behavior of Spies
In the previous lesson, we learned a lot about how to stub the behavior of Mock methods. In this lesson, we will cover the following recipes:
- Stubbing methods that return values
- Stubbing methods so that they throw exceptions
- Stubbing methods so that they return custom answers
- Stubbing void methods
Mockito is all about making mocks and stubbing their behavior. In comparison to the previous lesson, which focused on the stubbing behavior of mocks, in this lesson we will take a look at partial mocks, also known as spies. Spies are mocks that by default call real implementations. Additionally, you can also perform verification on such objects.
As told in the previous lesson, we will not verify implementation but verify whether the object under the test's logic does what it should do. Say that when we change some algorithm inside the service but at the end of the day we want the object under test to work in exactly the same manner; if we test the implementation of our methods and not the behavior of the system, our test will fail. In other words, in the majority of cases, we don't want to know exactly how something is done—we should only know what its outcome is.
Stubbing methods that return values
This section will explain how to stub a method that returns a value so that it returns our expected result.
To start, our system under test will be AverageTaxFactorCalculator along with the TaxFactorGetter class. Together, they form a unit whose purpose is to calculate the average factor. It calculates an average out of those values. Let’s have a look at the following sample:
public class AverageTaxFactorCalculator {
private final TaxFactorGetter taxFactorGetter;
public AverageTaxFactorCalculator(TaxFactorGetter taxFactorGetter) {
this.taxFactorGetter = taxFactorGetter;
}
public float calculateAvgTaxFactorFor(User user) {
float taxFactor = taxFactorGetter.getTaxFactorFromDb(user);
float anotherTaxFactor = taxFactorGetter.getTaxFactorFor(user);
return (taxFactor + anotherTaxFactor) / 2;
}
}
The implementation of the TaxFactorGetter looks as follows:
public class TaxFactorGetter {
static final float NO_COUNTRY_TAX_FACTOR = 0.3;
static final float DEFAULT_TAX_FACTOR = 0.5;
static final float DB_TAX_FACTOR = 0.8;
public float getTaxFactorFor(User user) {
if (user.isCountryDefined()) {
return DEFAULT_TAX_FACTOR;
}
return NO_COUNTRY_TAX_FACTOR;
}
public float getTaxFactorFromDb(User user) {
// simulation of DB access
return DB_TAX_FACTOR;
}
}
How to do it?
We would like to test our system as a whole without calling the database, so we will have to only partially stub TaxFactorGetter. Let’s follow a few simple steps to achieve this:
1. Call
Mockito.doReturn(value).when(spy).methodToStub().
2. Whichever approach we choose, we need to provide the desired output in the willReturn(...) or thenReturn(...) method, and pass the spy itself in the given(...) or when(...) method.
3. Remember that the last passed value during stubbing will be returned for each stubbed method call. Have a look at the following code:
willReturn(50,100).given(taxGetter).getTax();
4. As shown in the preceding line of code, regardless of the number of taxGetter.getTax() method executions, we will first return 50 and then always receive 100 (until stubbed again).
We have to bear in mind that if you try to stub a method with the BDDMockito.given(...).willReturn(...) call or in the standard manner—with the Mockito.when(...).thenReturn(...) call—then you will actually call the spy's method that you want to stub!
The following snippet depicts the aforementioned scenario for JUnit.
@RunWith(MockitoJUnitRunner.class)
public class AverageTaxFactorCalculatorTest {
@Spy TaxFactorGetter taxFactorGetter;
@InjectMocks AverageTaxFactorCalculator systemUnderTest;
@Test
public void should_calculate_avg_tax_factor_for_user_without_a_country() {
// given
float storedTaxFactor = 10;
float expectedAvgTaxFactor = 12;
willReturn(storedTaxFactor).given(taxFactorGetter).getTaxFactorFromDb(any(User.class));
// when
float avgTaxFactor = systemUnderTest.calculateAvgTaxFactorFor(new User());
// then
then(avgTaxFactor).isEqualTo(expectedAvgTaxFactor);
}
}
There's more...
Mockito allows us to provide a series of possible stubbed results either by using the fluent interface API or by means of varargs.
If we need to pass a series of return values to the stubbed spy's method using the fluent API, we need to stub the method invocation as follows:
willReturn(obj1).willReturn(obj2).given(spy).methodToStub();
Or, if we want to use varargs, you will have do it as follows:
willReturn(obj1, obj2).given(spy).methodToStub();
Stubbing methods so that they throw exceptions
In this section, we will stub a method that returns a value so that it throws an exception.
Getting ready
This section will reuse the example from the previous lesson. We have a class that calculates an average value of tax factors (AverageTaxFactorCalculator) and TaxFactorGetter is the provider of those values. One of the values is picked from the database (and we'll stub that method). We will test those two classes as a unit. For the convenience (so that you don't scroll around the lessons too much), I'm showing you the classes here (don't worry, they're really small):
public class AverageTaxFactorCalculator {
private final TaxFactorGetter taxFactorGetter;
public AverageTaxFactorCalculator(TaxFactorGetter taxFactorGetter) {
this.taxFactorGetter = taxFactorGetter;
}
public float calculateAvgTaxFactorFor(User user) {
float taxFactor = taxFactorGetter.getTaxFactorFromDb(user);
float anotherTaxFactor = taxFactorGetter.getTaxFactorFor(user);
return (taxFactor + anotherTaxFactor) / 2;
}
}
And its collaborator, TaxFactorGetter, is as follows:
public class TaxFactorGetter {
static final float NO_COUNTRY_TAX_FACTOR = 0.3;
static final float DEFAULT_TAX_FACTOR = 0.5;
static final float DB_TAX_FACTOR = 0.8;
public float getTaxFactorFor(User user) {
if (user.isCountryDefined()) {
return DEFAULT_TAX_FACTOR;
}
return NO_COUNTRY_TAX_FACTOR;
}
public float getTaxFactorFromDb(User user) {
// simulation of DB access
return DB_TAX_FACTOR;
}
}
How to do it?
To make our spy throw an exception instead of executing the real logic, you have to follow these simple steps:
1. Call
Mockito.doReturn(value).when(spy).methodToStub().
2. Whichever approach you've chosen, you have to provide the desired output in willReturn(...) or thenReturn(...), and pass the spy itself in the given(...) or when(...) method.
3. Remember that the value that was passed last during stubbing will be returned for each stubbed method call. Have a look at the following line of code:
4. willThrow(exception1, exception2).given(taxGetter).getTax();
5. As shown in the preceding line of code, regardless of the number of taxGetter.getTax() method executions, you will first throw exception1 and then always throw exception2 (until stubbed again).
We need to bear in mind that if we try to stub a method with the BDDMockito.given(...).willReturn(...) call or in the standard we stub a method with the Mockito.when(...).thenReturn(...) call—then we will actually call the spy's method that we need to stub!
Let's check the JUnit test, as follows:
@RunWith(MockitoJUnitRunner.class)
public class AverageTaxFactorCalculatorTest {
@Spy TaxFactorGetter taxFactorGetter;
@InjectMocks AverageTaxFactorCalculator systemUnderTest;
@Test
public void should_throw_exception_while_trying_to_calculate_mean_tax_factor() {
willThrow(new TaxFactorFetchException()).given(taxFactorGetter).getTaxFactorFor(any(User.class));
when(systemUnderTest).calculateAvgTaxFactorFor(new User());
thenThrown(TaxFactorFetchException.class);
}
}
How it is done?
Mockito allows us to provide a series of possible thrown exceptions to the stubbed method, either by using the fluent interface API or by means of varargs.
If we need to throw a series of exceptions from the stubbed spy's method using the fluent API, you will have to stub the method invocation as follows:
willThrow(ex1).willThrow(ex2).given(spy).methodToStub()
Or, if you want to use varargs, you have do it as follows:
willThrow(ex1, ex2).given(spy).methodToStub()(ex1, exj2).given(spy).methodToStub()
NOTE
Note that we are not passing an additional expected parameter to the @Test annotation (the expected parameter suggests that if a test ends by throwing an exception of the given type, then the test has ended successfully). In the majority of cases, you would want to control where the exception is thrown from (otherwise, your test could pass when it shouldn't). That is why, either you should use the try-catch approach (if an exception has not been thrown, the test should fail with a given message), the ExpectedException JUnit rule, or the catch-exceptionlibrary.
Stubbing methods so that they return custom answers
In this section, we will stub a method that returns a value so that it returns a custom answer of our choice.
This section is the last that will reuse the example from the previous section, which is related to a class that calculates an average value of tax factors. The starting point is the AverageTaxFactorCalculator class and its collaborator is TaxFactorGetter, which is the provider of those values. The latter class picks one of the tax factors from the database (we'll stub that method). We will test those two classes as a unit. For the sake of convenience, even though it violates the don't repeat yourself (DRY) principle, we will see the classes as follows so that you don't have to scroll around the page too much:
public class AverageTaxFactorCalculator {
private final TaxFactorGetter taxFactorGetter;
public AverageTaxFactorCalculator(TaxFactorGetter taxFactorGetter) {
this.taxFactorGetter = taxFactorGetter;
}
public float calculateAvgTaxFactorFor(User user) {
float taxFactor = taxFactorGetter.getTaxFactorFromDb(user);
float anotherTaxFactor = taxFactorGetter.getTaxFactorFor(user);
return (taxFactor + anotherTaxFactor) / 2;
}
}
We can find the TaxFactorGetter collaborator class in the following sample:
public class TaxFactorGetter {
static final float NO_COUNTRY_TAX_FACTOR = 0.3;
static final float DEFAULT_TAX_FACTOR = 0.5;
static final float DB_TAX_FACTOR = 0.8;
public float getTaxFactorFor(User user) {
if (user.isCountryDefined()) {
return DEFAULT_TAX_FACTOR;
}
return NO_COUNTRY_TAX_FACTOR;
}
public float getTaxFactorFromDb(User user) {
// simulation of DB access
return DB_TAX_FACTOR;
}
}
How to do it...
We'll stub the method that accesses the database in such a way that we will register a callback (an answer) that will check if the user has provided information about his country of origin. Based on that piece of data, we will return a specific value. To do this, you have to perform the following steps:
1. Call
Mockito.doAnswer(answer).when(spy).methodToStub().
2. Whichever approach you've chosen, you have to provide the answer to be executed in willAnswer(...) or doAnswer(...), and pass the spy itself in the given(...) or when(...) method.
3. Remember that the exception that was passed last during stubbing will be thrown for each stubbed method call. Have a look at the following line of code:
4. willAnswer(answer1, answer2).given(taxGetter).getTax();
5. As shown in the preceding line of code, regardless of the number of taxGetter.getTax() method executions, you will first throw exception1 and then always throw exception2 (until stubbed again).
Let's check the JUnit test. Have a look at the following sample:
@RunWith(MockitoJUnitRunner.class)
public class AverageTaxFactorCalculatorTest {
@Spy TaxFactorGetter taxFactorGetter;
@InjectMocks AverageTaxFactorCalculator systemUnderTest;
@Test
public void should_return_incremented_tax_factor_while_trying_to_calculate_mean_tax_factor_for_a_user_from_undefined_country() {
// given
final float expectedTaxFactor = 107;
willAnswer(withTaxFactorDependingOnUserOrigin()).given(taxFactorGetter).getTaxFactorFromDb(any(User.class));
// when
float avgTaxFactor = systemUnderTest.calculateAvgTaxFactorFor(new User());
// then
then(avgTaxFactor).isEqualTo(expectedTaxFactor);
}
private Answer<Object> withTaxFactorDependingOnUserOrigin() {
return new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
float baseTaxFactor = 50;
float incrementedTaxFactor = 200;
if (invocation.getArguments().length > 0) {
User user = (User) invocation.getArguments()[0];
if (!user.isCountryDefined()) {
return incrementedTaxFactor;
}
}
return baseTaxFactor;
}
};
}
}
How it is done?
Mockito allows you to provide a series of possible answers to the stubbed method, either by using the fluent interface API or by means of varargs.
If we need to execute a series of answers from the stubbed spy's method using the fluent API, we will have to stub the method invocation as follows:
willAnswer(answer1).willAnswer(answer2).given(spy).methodToStub()
Or, if you want to use varargs, you'd have do it as follows:
willAnswer(answer1, answer2).given(spy).methodToStub()
Stubbing void methods
In this section, we will stub a void method. A void method is one that doesn't return a value. Note that since we want to partially stub a mock, it most likely means that our class is doing too much, and that is quite true for this scenario. It is best practice to not write such cod—always try to follow the SOLID principles.
For this section, our system under test will be the UserDataUpdator class, which delegates most of the work to its collaborator, TaxFactorService. The latter calculates the mean value of the tax factor (for simplicity, it's a fixed value) and then updates the user's tax data via a web service (since it's a simple example, we do not have any real web service calls):
public class UserDataUpdator {
private final TaxFactorService taxFactorService;
public UserDataUpdator(TaxFactorService taxFactorService) {
this.taxFactorService = taxFactorService;
}
public boolean processTaxDataFor(User user) {
try {
float meanTaxFactor = taxFactorService.calculateMeanTaxFactor();
taxFactorService.updateMeanTaxFactor(user, meanTaxFactor);
return true;
} catch (ConnectException exception) {
System.err.printf("Exception occurred while trying update user data [%s]%n", exception);
throw new TaxFactorConnectionException(exception);
}
}
}
The TaxFactorService class is shown in the following code (note that updateMeanTaxFactor is throwing a checked exception, ConnectException):
public class TaxFactorService {
private static final float MEAN_TAX_FACTOR = 0.5;
public void updateMeanTaxFactor(User user, float meanTaxFactor) throws ConnectException {
System.out.printf("Updating mean tax factor [%s] for user with defined country%n", meanTaxFactor);
}
public float calculateMeanTaxFactor() {
return MEAN_TAX_FACTOR;
}
}
How to do it...
To stub a spy's void method in such a way that it does nothing, you have to perform the following steps:
1. Call
Mockito.doNothing().when(spy).methodToStub().
2. Whichever approach you've chosen, willDoNothing() or doNothing(), you will pass the spy itself in the given(...) or when(...) method.
3. Remember that the exception that was passed last during stubbing will be thrown for each stubbed method call. Have a look at the following code:
4. willDoNothing().willThrow(exception).given(taxGetter).getTax();
5. As shown in the preceding code, regardless of the number of taxGetter.getTax() method executions, the method will first do nothing and then always throw an exception (until stubbed again).
Have a look at the following snippet JUnit test:
@RunWith(MockitoJUnitRunner.class)
public class UserDataUpdatorTest {
@Spy TaxFactorService taxFactorService;
@InjectMocks UserDataUpdator systemUnderTest;
@Test
public void should_successfully_update_tax_factor_for_user() throws ConnectException {
// given
willDoNothing().given(taxFactorService).updateMeanTaxFactor(any(User.class), anyDouble());
// when
boolean success = systemUnderTest.processTaxDataFor(new User());
// then
then(success).isTrue();
}
}
How it is done?
Say we want to make a method first throw an exception and do that only once; after that, you want the method to do nothing. Take a look at the following snippet, which shows how to achieve this:
willThrow(exception).willNothing().given(spy).methodToSpy();
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