In our last unit, we talked about using Stub classes for testing. However, stub classes require a lot of extra code to create, and if we want to create multiple test cases, we would need to create multiple stub classes. This approach therefore is clunky and heavy-weight.
Enter mockito
, a framework for improving unit testing. Mockito a can support “on the fly” stubbing within the test. In this module, we will look at example uses, as well as show some features for mockito.
Here is GoodAbbreviationsTest
, but using mockito rather than a mock class.
package edu.virginia.cs.nbateams;
import org.junit.jupiter.api.*;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class GoodAbbreviationsTest {
private NBATeamReader reader;
private static NBATeam LAKERS, CELTICS;
@BeforeAll
public static void init() {
LAKERS = new NBATeam(1,"Lakers","Los Angelos","LAL",
Conference.WESTERN, Division.PACIFIC);
CELTICS = new NBATeam(2, "Celtics", "Boston", "BOS",
Conference.EASTERN, Division.ATLANTIC);
}
@BeforeEach
public void setup() {
reader = mock(NBATeamReader.class);
}
@Test
public void testGoodAbbreviations() {
when(reader.getNBATeams()).thenReturn(List.of(LAKERS, CELTICS));
GoodAbbreviations abbreviations = new GoodAbbreviations();
List<NBATeam> expected = List.of(CELTICS);
List<NBATeam> goodAbbreviationsTeams = abbreviations.extractGoodAbbreviationTeams(reader);
assertEquals(expected, goodAbbreviationsTeams);
}
}
Look at the line:
reader = mock(NBATeamReader.class);
What we are saying here is that we want to create a Test Double object that has the same interface as NBATeamReader
. This creates our mock object that we will use in this test class.
Look at the first line of our test function:
when(reader.getNBATeams()).thenReturn(List.of(LAKERS, CELTICS));
What does this line mean? Well, in prose, we would read it as “When reader.getNBATeams()
is called, then return a list of the LAKERS and CELTICS teams”.
In short, we are creating a stub-function here! We no longer need the heavy-weight namespace consuming class StubNBATeamReader
.
Instead, we are creating our stub right here!
Installing mockito in a gradle project, like any other library is simple. You can simply add:
testImplementation 'org.mockito:mockito-core:4.11.0'
testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0'
To your dependencies. You can, of course, use a more recent version number if you wish (This uses version 4.11). A note that for now we are using mockito-core
with mockito-junit-jupiter
in order to optimize for JUnit 5.
For this class, because we are using Java 17, make sure you are using Mockito 4, not Mockito 5. I specifically am using Version 4.11.0 of mockito-core and Version 4.11.0 of mockito-junit-jupiter which is compatiable with JUnit Jupiter (aka, JUnit 5). I have had difficulty making Mockito 5 work, as it requires some additional setup. For ease, in this class, I’m encouraging everyone to use Mockito 4.11.
import static org.mockito.Mockito.*;
This gives you access to the functions in mockito we want to use.
Let’s now consider that we want to integrate GoodAbbreviations
with NBATeamReader
and test to ensure it works. Now, instead of mocking the behavior of NBATeamReader
, we instead mock the behavior of BallDontLieReader
, and use the actual NBATeamReader
class.
package edu.virginia.cs.nbateams;
import org.junit.jupiter.api.*;
import org.json.*;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class GoodAbbreviationsIntegrationTest {
private GoodAbbreviations goodAbbreviations;
private NBATeamReader nbaTeamReader;
private BallDontLieReader mockBDLReader;
private static final NBATeam CELTICS = new NBATeam(2, "Celtics", "Boston", "BOS",
Conference.EASTERN, Division.ATLANTIC);
@BeforeEach
public void setup() {
goodAbbreviations = new GoodAbbreviations();
mockBDLReader = mock(BallDontLieReader.class);
nbaTeamReader = new NBATeamReader(mockBDLReader);
}
@Test
public void testGoodAbbreviationsFromNBATeamReader() {
JSONObject mockJSONObject = getMockJSONObject();
when(mockBDLReader.getAllNBATeams()).thenReturn(mockJSONObject);
List<NBATeam> goodAbbrvTeams = goodAbbreviations.extractGoodAbbreviationTeams(nbaTeamReader);
assertIterableEquals(goodAbbrvTeams, List.of(CELTICS));
}
private JSONObject getMockJSONObject() {
String mockJSONString = """
{
"data":[
{"id":2,"abbreviation":"BOS","city":"Boston","conference":"East","division":"Atlantic",
"full_name":"Boston Celtics","name":"Celtics"},
{"id":14,"abbreviation":"LAL","city":"Los Angeles","conference":"West","division":"Pacific",
"full_name":"Los Angeles Lakers","name":"Lakers"}
]
}
""";
JSONObject teamsJSONObject = new JSONObject(mockJSONString);
return teamsJSONObject;
}
}
While this may look like a lot, realize that getMockJSONObject
is only used to create the mock JSONObject
that is in the same format of a JSONObject we would expect from BallDontLieReader
. However, from here, no mocking is done inside of the classes NBATeamReader
or GoodAbbreviations
. In this way, we have gone one step in a top-down integration, downwards from GoodAbbreviation (because GoodAbbreviation could itself be used by another class, this could be an example of part of a sandwich integration where GoodAbbreviations
is the target layer).
At this point, you may notice that I haven’t focused on the external dependency of JSONObject and JSONArray. This is intentional, as we will cover JSON in later units. However, JSONObject and JSONArray are, in effect, data structures that are not dissimilar from Maps and Lists. While we have a unit on the org.json library later, it’s worth looking at the structure of a JSON file for now:
{"professor" : {
"name" : {
"first" : "Paul",
"last" : "McBurney",
"preferred" : "Will"
},
"degrees" : [
{ "degree" : "BS",
"subject" : "Computer Science",
"year" : 2010,
"institution" : "West Virginia University"
},
{ "degree" : "MS",
"subject" : "Computer Science",
"year" : 2012,
"institution" : "West Virginia University",
"advisor" : "Tim Menzies"
},
{ "degree" : "Ph.D.",
"subject" : "Computer Science",
"year" : 2016,
"institution" : "University of Notre Dame",
"advisor" : "Collin McMillan"
}
]
}
}
This structure, fundamentally, is a combination of Maps and Lists. For example:
Because the JSON format is highly standardized and stable, I can
still feel comfortable using real JSON objects in testing, and
therefore the org.json
library. There are purists who will
say we should unit-test by mocking the behavior of that library,
and there are fair reasons for that argument. For example, if
the JSON format were less well-establish and the structure of files
could change, or org.json were still new and the interfaces could change, then our tests could potentially fail because of things outside of our code. However, to me, it’s fundamentally no different than testing using ArrayLists, HashMaps, or Strings.
A mock is very similar to a stub. We are creating an object like a stub object to remove the need for external dependencies. However, we still want to monitor to verify that the class interacts with external dependencies as expected For example, the class NBATeamExcelWriter
writes to an ExcelWorkbook and saves it. Instead of actually saving the file, and then reading the saved file back-in to ensure it is correct, we can instead check to see if the file “tries” to save, but block the actual file I/O operation.
Below is an example of using mocking a class that relies heavily on an internal Map
. One limitation of testing our classes with abstract data types is that, typically, we have to pick a concrete implementation (like TreeMap
or HashMap
) to do testing. This can cause problems, because it makes our test inflexible, since they are tied to specific concrete types. If the underlying concrete type used by the object changes, this could break our tests.
Consider the Patron
class from our Library
example below. For the sake of brevity, I only included the constructor we plan to use and method we plan to test.
public class Patron {
private final int id;
private String firstName, lastName;
private List<Book> booksCheckedOut;
public Patron(int id, String firstName, String lastName, List<Book> booksCheckedOut) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.booksCheckedOut = booksCheckedOut;
}
public void addBookToCheckedOut(Book b) {
if (booksCheckedOut.contains(b)) {
throw new IllegalArgumentException("Already have copy checked out");
}
booksCheckedOut.add(b);
}
}
In this case, we can test our method addBookToCheckedOut
method by mocking the List. That’s right, we can mock the abstract data type List
, meaning we can test our class while keeping our test independent of the concrete List implementation used.
Here is an example of using a mocked List in PatronTest.java
, and it will show us the same when-then
syntax as before, as well as a new piece of Mockito syntax, verify
:
import org.junit.jupiter.api.*;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class PatronTest {
private Patron testPatron;
private List<Book> mockList;
private static final Book mistborn = new Book(3, "The Final Empire", "Brandon Sanderson");
@SuppressWarnings("unchecked") // used because we cannot mock List<Book>, only List, which produces warning
@BeforeEach
public void setup() {
mockList = mock(List.class);
testPatron = new Patron(1, "Jane", "Doe", mockList);
}
@Test
public void testCheckOutSuccessful() {
//when you ask the mocklist if it contains Mistborn, it says "no"
when(mockList.contains(mistborn)).thenReturn(false);
//call the method addBookToCheckedOut(mistborn)
testPatron.addBookToCheckedOut(mistborn);
//verify that the mock list had add(mistborn) called on it
verify(mockList).add(mistborn);
}
}
verify
functionverify
is used to verify **that a particular function, with a particular argument, was called on our mockList
object.
This happens in our Patron
’s addBookToCheckedOut
when we execute:
booksCheckedOut.add(b);
Because booksCheckedOut
in our testPatron
instance is the same as mockList
, and b
is the same book as mistborn
in our function call, this line does the same thing as saying:
mockList.add(mistborn)
This means, because we did have our test try to add mistborn
to mockList
, we can say that the function behaved as expected. Now, this may look odd, as we have a test with no explicit assert
statements. You may even think we should check to make sure booksCheckedOut
is in the state it should be. However, remember, we don’t have a real list. This is a mock list. However, the simple fact that the method tried to add mistborn
to the mocked checkOutBookList
is sufficient for our testing purposes.
verify
In the same way we have used assert
to check post-conditions on our function output and test objects in the past, we use verify
to check the post-conditions for mocked objects. This is because mocked objects do not have any real behavior. We cannot expect the mock object to behave correctly, since the entire point of mocking is to separate the method-under-test from the external dependency.
For a second example, let’s now consider the false case:
@Test
public void testCheckOutFailure_alreadyHaveBook() {
//when you ask the mocklist if it contains Mistborn, it says "yes"
when(mockList.contains(mistborn)).thenReturn(true);
//call the method addBookToCheckedOut(mistborn), should get exception
assertThrows(IllegalArgumentException.class, () ->
testPatron.addBookToCheckedOut(mistborn));
//verify that the mock list has never had add(mistborn) called on it
verify(mockList, times(0)).add(mistborn);
}
In this case, we use our old friend assertThrows
to ensure the method throws an exception. However, we also want to verify
that we have tried to add mistborn
to our mockList
zero times. The times(0)
argument is useful for saying how many times a function should have been called. If we don’t include a times
argument, then it defaults to times(1)
- that is, we call the function exactly one time. In the same way, you can use times(2)
, times(x)
where x is an int, etc.
Another way we could write this test is to say that, after our call to contains
, nothing should happen to this list at all. This is the same test, but with our last statement changed.
@Test
public void testCheckOutFailure_alreadyHaveBook() {
//when you ask the mocklist if it contains Mistborn, it says "yes"
when(mockList.contains(mistborn)).thenReturn(true);
//call the method addBookToCheckedOut(mistborn), should get exception
assertThrows(IllegalArgumentException.class, () ->
testPatron.addBookToCheckedOut(mistborn));
//verify that the mock list has never had add(mistborn) called on it
verify(mockList).contains(mistborn);
verifyNoMoreInteractions(mockList);
}
verifyNoMoreInteractions
means that no methods, other than the ones we have called verify
with, have been called. In this case, we have to include contains
since we did use it in the if-statement in Patron.
Be aware, however, the test above is quite brittle - that is, an underlying change to the code that doesn’t change the functionality of the method, but calls different, say, getters functions on the mock object could result in the test failing, even if the post-conditions of the object would be correct. As such, generally, I would recommend against using verifyNoMoreInteractions
, but it is a tool worth being aware of.
verify
vs. assert
methodsWhen we are testing return values of the method-under-test (when present) and post-conditions of test objects, we always want to use assert
methods like assertEquals
.
We only use verify
to ensure the intended destructive functions (that is, functions that could produce side effects on the external dependency) are called. For instance, where in the tests above we used…
verify(mockList).add(mistborn);
…we don’t care how mistborn
is added to the list, and we aren’t asserting the list now contains mistborn
. Because that behavior is dependent upon an external class. We only care that we interacted with the correct interface method that should add mistborn
to the list. The point is that this List is unit tested elsewhere, and we only want this test to fail if the fault is with our code in the method-under-test, not due to incorrect behavior in another class.
In short, verify is simply ensuring that the intended interaction is invoked. We don’t check if that interaction produces a correct outcome because, remember, this is not a real List! This is a mock of a List! It doesn’t have any real behavior!
But, for instance, I would never use any of the following:
verify(mockList).contains(mistborn)
verify(mockList).size()
verify(mockList).isEmpty()
Because none of the methods contains
, size
, or isEmpty
can produce side effects! So there is no value in verify
-ing them, or ensuring they run.
verify
should only be used on methods that can produce side effects as post-conditions.
You might right now be thinking that this is a lot of extra work to get a more exact unit test, why not just do integration tests? After all, if the larger parts work, then the small parts must work, right?
Remember: debugging a little code is significantly easier than debugging a lot of code! Using Stubs like this allows us to dramatically shrink the code we are looking at! We can even use this to customize our level of integration.
Yes, adding unit tests and integration tests does take more code, but this isn’t a problem. In fact, it’s important to understand that “less code” does not mean “less work”, as proper testing can drastically reduce our overall debugging work. The “code” of our stub is trivially easy to write, and it ensures that our test of extractGoodAbbreviationTeams
is stable and portable. This test no longer relies on other classes working, no longer relies on the internet, and no longer relies on an external server. Additionally, even if our implementation of the class NBATeamReader
changes, so long as the interface remains the same, this test will still work!
Ultimately, mockito allows us to write tests without needing to create a ton of extra dependencies, which will typically be more work than mocking.