In this unit, we’ll look at some more examples of Mockito to show it’s use, as well as look at some additional features.
Let’s consider the following Java classes:
public class Transcript {
private final Map<Section, Grade> history;
public Transcript() {
this(new HashMap<>());
}
protected Transcript(Map<Section, Grade> history) {
this.history = history;
}
public Grade getGrade(Section section) {
if (!history.containsKey(section)) {
throw new IllegalArgumentException("Transcript doesn't contain section: " + section);
}
return history.get(section);
}
public Optional<Grade> getBestGrade(Course course) {
return history.keySet().stream()
.filter(section -> course.equals(section.getCourse()))
.map(history::get)
.max(Comparator.comparing(Grade::getPrerequisiteScore));
}
public void add(Section section, Grade grade) {
history.put(section, grade);
}
public Set<Section> getSections() {
return Collections.unmodifiableSet(history.keySet());
}
}
Specifically, let’s say I want to test my Transcript code. The methods all depend all a class called Section, so I could make some Section objects for testing. The problem? Section is a really big class with a lot of additional dependencies.
public class Section {
private final int courseRegistrationNumber;
private final int sectionNumber;
private final Course course;
private final Semester semester;
private Location location;
private TimeSlot timeSlot;
private Lecturer lecturer;
private int enrollmentCapacity;
private int waitListCapacity;
private final Set<Student> enrolledStudents;
private final List<Student> waitListedStudents;
public Section(int courseRegistrationNumber, int sectionNumber, Course course, Semester semester,
Location location, TimeSlot timeSlot, Lecturer lecturer, int enrollmentCapacity, int waitListCapacity) {
this.courseRegistrationNumber = courseRegistrationNumber;
this.course = course;
this.semester = semester;
this.sectionNumber = sectionNumber;
this.location = location;
this.timeSlot = timeSlot;
this.lecturer = lecturer;
this.enrollmentCapacity = enrollmentCapacity;
this.waitListCapacity = waitListCapacity;
this.enrolledStudents = new HashSet<>();
this.waitListedStudents = new ArrayList<>();
}
//...lots of getters and setters, lots of other methods
}
To make a Section, I have to make at least a Course, a Semester, a Location, a TimeSlot, a Lecturer, and potentially several Student objects. This is an unavoidable dependency, but manually building this objects, especially into the exact testable state I want them in, is going to take a lot of work. Additionally, any logic errors in Section or any of its myriad dependencies could cause my tests to fail even if my Transcript is implemented correctly.
So, instead of making these objects, I can use Mockito to mock the Section and Course I need. For instance, let’s look at some existing tests for add and getGrade which mock Section.
@ExtendWith(MockitoExtension.class)
class TranscriptTest {
@Mock
HashMap<Section, Grade> history;
@Mock
Section mockSection, sectionAlgorithmsFail, sectionAlgorithmsPass, sectionSde;
@Mock
Course algorithms, sde;
@Test
void getGrade() {
when(history.containsKey(mockSection)).thenReturn(true);
when(history.get(mockSection)).thenReturn(Grade.A);
var transcript = new Transcript(history);
assertEquals(Grade.A, transcript.getGrade(mockSection));
}
@Test
void getGrade_notPresent() {
when(history.containsKey(mockSection)).thenReturn(false);
var transcript = new Transcript(history);
assertThrows(IllegalArgumentException.class, () -> transcript.getGrade(mockSection));
}
@Test
void add() {
var transcript = new Transcript(history);
transcript.add(mockSection, Grade.C);
verify(history).put(mockSection, Grade.C);
}
}
A note that in the code snippet above, I’m using slightly difference syntax than the last unit. I’m using the MockitoExtensions with allows for the use of the @Mock annotation. Note that you must have @ExtendWith(MockitoExtension.class) before the class declaration in order to be able to use @Mock in this way.
Specifically, looking at the declarations above:
@Mock
HashMap<Section, Grade> history;
@Mock
Section mockSection, sectionAlgorithmsFail, sectionAlgorithmsPass, sectionSde;
@Mock
Course algorithms, sde;
This allows to define my mock objects just like I could define any test objects, but without having to explicitly add the code history = mock(Map.class) into a test or setup function. The object is simply assumed to be a mock. The same is true for the other mocks.
Let’s look specifically at the first test for getGrade
@Test
void getGrade() {
when(history.containsKey(mockSection)).thenReturn(true);
when(history.get(mockSection)).thenReturn(Grade.A);
var transcript = new Transcript(history);
assertEquals(Grade.A, transcript.getGrade(mockSection));
}
Remember that history is a mock object, specifically a Map<Section,Grade>. mockSection is a mock of the Section class. Specifically, I am telling history to do the following:
mockSection, return truemockSection, return Grade.AFrom there, I inject history into a test Transcript object via the constructor. Then, I call transcript.getGrade(mockSection), expecting it to return A.
In this what, I have successfully tested the logic of getting a Grade for a Section from a particular Transcript without having to actually create a Section, along with it’s Course, Semester, Location, etc.
Remember, what am I testing?. I am testing the Transcript method getGrade(Section section). I am not testing Section, Course, or even Grade. I am only testing the following logic:
public Grade getGrade(Section section) {
if (!history.containsKey(section)) {
throw new IllegalArgumentException("Transcript doesn't contain section: " + section);
}
return history.get(section);
}
This test ensures that my I bypass the if statement if the transcript contains a section, and returns the result of getting that Grade. Similarly, the next test checks the result of the if statement being true:
@Test
void getGrade_notPresent() {
when(history.containsKey(mockSection)).thenReturn(false);
var transcript = new Transcript(history);
assertThrows(IllegalArgumentException.class, () -> transcript.getGrade(mockSection));
}
Here, I am telling history to say it does not have a grade for mockSection, which should result in an error being thrown. The assertThrows ensures that an IllegalArgumentException is throw when we call transcript.getGrade().
For testing add(Section section, Grade grade), we use:
@Test
void add() {
var transcript = new Transcript(history);
transcript.add(mockSection, Grade.C);
verify(history).put(mockSection, Grade.C);
}
Here, we are using verify to ensure that, as a result of adding the mockSection with a Grade.C to our Transcript, that that content is passed to our history dependency. Remember, we use verify to ensure that the correct post condition is envoked in our dependency, that is that the history map has the key-value mockSection and Grade.C added to it.
Without mocking history (but still mocking the sections) we could do the following:
@Test
void add() {
history = new HashMap<>();
var transcript = new Transcript(history);
transcript.add(mockSection, Grade.C);
assertEquals(Grade.C, history.get(mockSection));
}
This is equivalent in terms of functionality as my above test where I did mock history. In both above cases, mocking still saves us the trouble of actually making the Section objects with all their dependencies, which is the main thing we want to avoid. It’s debatable if mocking the history HashMap saves us much trouble, since we could initialize the object with Map.of.
In practice, I would probably create real HashMaps in this case rather than mocking, but since this is educational, I am mainly wanting to illustrate how to use mocks, so I’m erring on the side of more mocks for this module.
The main takeaway, however, is this:
assertEquals or another assert functions.verify to ensure the correct function is invoked.You cannot meaningfully use assertEquals on mocked objects to check post conditions, because the mock objects do not have any real behavior. They are simply a convenient placeholder for a real object, mocking just enough behavior for us to test the unit we care about.
As a side not, in general, you should never mock the “Class-Under-Test” (in this case, Transcript) because the point of these tests is to test the real behavior of Transcript. So while I might mock Transcript in other test classes, I will never mock it in TranscriptTest.java. There may be some exceptions to this rule, but they are very rare.
Now we want to test the function getBestGrade():
public Optional<Grade> getBestGrade(Course course) {
return history.keySet().stream()
.filter(section -> course.equals(section.getCourse()))
.map(history::get)
.max(Comparator.comparing(Grade::getPrerequisiteScore));
}
The point of this function is to get the best grade that a student has in a Course. While most students only take a Course (that is, only one Section of a Course) one time, a student could repeat a Course if they failed it. At that point, we only want to count the “best grade” when looking at prerequisites. So, the above code will filter the Transcript history map to only look at Sections of the input Course. We could test this code with the following.
@Test
void getBestGrade_multipleTakes() {
history = new HashMap<>(Map.of(
sectionAlgorithmsFail, Grade.F,
sectionAlgorithmsPass, Grade.B_PLUS,
sectionSde, Grade.A
));
when(sectionAlgorithmsFail.getCourse()).thenReturn(algorithms);
when(sectionAlgorithmsPass.getCourse()).thenReturn(algorithms);
when(sectionSde.getCourse()).thenReturn(sde);
var transcript = new Transcript(history);
var bestGradeOptional = transcript.getBestGrade(algorithms);
assertTrue(bestGradeOptional.isPresent());
assertEquals(Grade.B_PLUS, bestGradeOptional.get());
}
To explain this test, we have two different Sections that were offerings of the Course algorithms. Additionally, we have one Section that is not part of algorithms but is instead a different Course, sde. I use real course names from UVA for my test cases simply because it helps me understand the metaphor better (as opposed to mockCourse1, mockCourse2, etc.).
Be aware that by default, my Section mock objects have no Course, since they don’t have any real behavior. As such, I have to tell them what their course is via the when operation.
when(sectionAlgorithmsFail.getCourse()).thenReturn(algorithms);
when(sectionAlgorithmsPass.getCourse()).thenReturn(algorithms);
when(sectionSde.getCourse()).thenReturn(sde);
Here I’m saying that sectionAlgorithmsFail and sectionAlgorithmsPass are both part of the course algorithms, while sectionSde is part of the course sde
In this I’m expecting the best grade for algorithms to be a B_PLUS, since it is the best of the two grades this Transcript has for algorithms. However, by default, the Java stream terminal operation max returns an Optional<Grade> on a Stream<Grade>. This is because it’s possible that the stream is empty by the time we reach the terminal, meaning there is no max.
That said, Optionals are fairly straight forward, and we can use the boolean function isPresent to assert the optional actually has a grade, than then use the get function to actually get the Grade for testing:
var bestGradeOptional = transcript.getBestGrade(algorithms);
assertTrue(bestGradeOptional.isPresent());
assertEquals(Grade.B_PLUS, bestGradeOptional.get());
Similarly, we can use a test to ensure that when there is no Section on the student’s Transcript that is related to Course, we return no grade at all:
@Test
void getBestGrade_notPresent() {
history = new HashMap<>(Map.of(
sectionAlgorithmsFail, Grade.F,
sectionAlgorithmsPass, Grade.B_PLUS
));
when(sectionAlgorithmsFail.getCourse()).thenReturn(algorithms);
when(sectionAlgorithmsPass.getCourse()).thenReturn(algorithms);
var transcript = new Transcript(history);
assertEquals(Optional.empty(), transcript.getBestGrade(sde));
}
Specifically, Optional.empty() is the result of an Optional that doesn’t contain anything. In this case, because I asked for the best sde grade, and there is no sde related Section in history, this returns Optional.empty().
Both of my tests pass in this case. Again, notice I never use verify. This is because this method should have no post-conditions, or side-effects. This method simply queries the data to get the best grade. It doesn’t change any data.
Now that we have this function in place, we can look at the Prerequisite class. Each Course has a Prerequisite, which lists zero or more required classes that must have been taken before you can register for a course, as well as the minimum grade in that course.
public class Prerequisite {
private final Map<Course, Grade> requiredCourses;
public Prerequisite() {
this(new HashMap<Course, Grade>());
}
public Prerequisite(Map<Course, Grade> requiredCourses) {
this.requiredCourses = requiredCourses;
}
public void add(Course course, Grade minimumGrade) {
requiredCourses.put(course, minimumGrade);
}
public Grade getMinimumGrade(Course course) {
if (!requiredCourses.containsKey(course)) {
throw new IllegalArgumentException("Prerequisites doesn't include course: " + course);
}
return requiredCourses.get(course);
}
public void remove(Course course) {
if (!requiredCourses.containsKey(course)) {
throw new IllegalArgumentException("Course: " + course + " is not in the prerequisites: " + this);
}
requiredCourses.remove(course);
}
public Set<Course> getPrerequisiteCourses() {
return Collections.unmodifiableSet(requiredCourses.keySet());
}
public boolean isMetBy(Transcript transcript) {
for (Course course: requiredCourses.keySet()) {
var minimumGrade = requiredCourses.get(course);
var optionalTranscriptGrade = transcript.getBestGrade(course);
if (optionalTranscriptGrade.isEmpty()) {
return false;
}
var transcriptGrade = optionalTranscriptGrade.get();
if (!transcriptGrade.greaterThanOrEqualTo(minimumGrade)) {
return false;
}
}
return true;
}
}
For now, let’s focus on the method isMetBy(Transcript transcript). This method checks whether a particular Transcript has met the prerequisites. This means the student has taken every class in the set of requiredCourse and has received at least the minimum required grade for that course.
At UVA, our algorithms course, CS 3100 (aka, DSA2) has two prerequisites: CS 2100 (aka dsa1) and CS 2120 (aka dmt1, or Discrete Math) with a C- or better. We design the test case to follow this idea below:
class PrerequisiteTest {
@Mock
private Course dsa1, dmt1;
@Mock
Map<Course, Grade> requiredCourses;
private Prerequisite prerequisite;
@Test
void isMetBy_true() {
var transcript = mock(Transcript.class);
when(transcript.getBestGrade(dsa1)).thenReturn(Optional.of(Grade.A));
when(transcript.getBestGrade(dmt1)).thenReturn(Optional.of(Grade.C_MINUS));
requiredCourses = Map.of(
dsa1, Grade.C_MINUS,
dmt1, Grade.C_MINUS);
prerequisite = new Prerequisite(requiredCourses);
assertTrue(prerequisite.isMetBy(transcript));
}
}
In this above test, dsa1 and dmt are mock Course. We create our test prerequisite object using requiredCourses. We used a real Map for the requiredCourses, stating both mock courses require a Grade.C_Minus or better.
Our Transcript is itself a mock object. In this case, we are mocking the results of the getBestGrade method, saying that, for this transcript, the best grade for dsa1 is an A, and the best grade for dmt1 is a C-.
The method under test, isMetBy is called in the last line, assertTrue(prerequisite.isMetBy(transcript)) since this Transcript meets the prerequisites.
Now let’s look at one false case where the false return is caused specifically by a student’s best grade being below the minimum required grade:
@Test
void isMetBy_false_lowGrade() {
var transcript = mock(Transcript.class);
when(transcript.getBestGrade(dsa1)).thenReturn(Optional.of(Grade.D_PLUS));
lenient().when(transcript.getBestGrade(dmt1)).thenReturn(Optional.of(Grade.A));
requiredCourses = Map.of(
dsa1, Grade.C_MINUS,
dmt1, Grade.C_MINUS);
prerequisite = new Prerequisite(requiredCourses);
assertFalse(prerequisite.isMetBy(transcript));
}
This test is nearly identical to our true test case, with the exception being that the best grade for dsa is now a D+, which is worse than the required C-. As such this should return false, as you can see at the end of the method.
lenient() keywordI want to quickly address the lenient() keyword in the above test:
lenient().when(transcript.getBestGrade(dmt1)).thenReturn(Optional.of(Grade.A));
Here, lenient() does not directly affect the test case. This is basically the same thing as saying:
when(transcript.getBestGrade(dmt1)).thenReturn(Optional.of(Grade.A));
However, a rule of mockito usage is that you should only when-then things that the method under test actually does. You should never add unnecessary when-then.
When I remove this lenient() from this line:
var transcript = mock(Transcript.class);
when(transcript.getBestGrade(dmt1)).thenReturn(Optional.empty());
when(transcript.getBestGrade(dsa1)).thenReturn(Optional.of(Grade.A));
the logic of the test doesn’t change. However, mockito may crash with the following error message:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at sde.virginia.edu.hw4.PrerequisiteTest.isMetBy_false_missingClass(PrerequisiteTest.java:112)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
The reason this happens is that, when the test is running, it is possible, depending on the order isMetBy loops through the requiredCourses Map that we never actually call: transcript.getBestGrade(dsa1). This is because if we check the grade for dmt1 first, then we have already failed the meet the pre-requisite, and so the function returns false without every checking dsa1. The MockitoExtensions sees this, and intentionally causes the test to fail, since there was a when-then we didn’t use.
Since we cannot easily force the order that the course grades are checked in (and we wouldn’t want to, since that’s an implementation detail that could easily change), instead, we use lenient
var transcript = mock(Transcript.class);
when(transcript.getBestGrade(dsa1)).thenReturn(Optional.of(Grade.D_PLUS));
lenient().when(transcript.getBestGrade(dmt1)).thenReturn(Optional.of(Grade.A));
This tells the MockitoExtensions that “Hey, it’s find if we never actually check dmt1. Don’t fail the test over it.” Thus, lenient() is a way to say “it’s possible this when-then isn’t needed.”
However, we do not want to add lenient() to the dsa1 when-then, since we absolutely want to ensure that isMetBy checks the dsa1 grade (the grade that causes the false return). In general, I only use lenient when I need to, and this is typically caused by data being stored in Set or Map where the iteration order is intentionally unpredictable.
Annoyingly, without lenient(), the test will not always fail. This is because roughly half the time, dmt1 will be checked before dsa1, meaning that there were no unnecessary when-thens. However, if dsa1 is checked before dmt1, then dmt1 is never actually checked because we return false without finishing the loop.
We also want our isMetBy method to return false if the student has no grade for a specific course (implying they have never taken it).
@Test
void isMetBy_false_missingClass() {
var transcript = mock(Transcript.class);
when(transcript.getBestGrade(dmt1)).thenReturn(Optional.empty());
lenient().when(transcript.getBestGrade(dsa1)).thenReturn(Optional.of(Grade.A));
requiredCourses = Map.of(dsa1, Grade.C_MINUS, dmt1, Grade.C_MINUS);
prerequisite = new Prerequisite(requiredCourses);
assertFalse(prerequisite.isMetBy(transcript));
}
In this case, we use Optional.empty() to signal that this transcript has no grade for dmt1. Once again, it’s possible this method returns false without ever checking dsa1 due to not being able to predict the order the courses are iterated through, so we use the lenient keyword to let mockito know not to fail the test if that when-then isn’t used.
Switching gears to some unrelated code, I want to consider the following code adapted from the Three Layer Architecture module:
public class BestSellersService {
public Book getLongestCurrentBestSeller(ListName listName) {
var dataManager = new BestSellersDataManager();
var bestSellers = dataManager.getCurrentBestSellerList(listName);
var longestCurrentBook = bestSellers.getBooks().stream()
.max(Comparator.comparing(Book::getWeeksOnList));
if (longestCurrentBook.isEmpty()) {
throw new EmptyBestSellerListException(bestSellers.toString());
}
return longestCurrentBook.get();
}
}
For the sake of testing the logic of this method which is getting the longest running Book on a BestSellerList, we might start thinking of writing a test like this:
BestSellersDataManagerBut wait…you remember that the BestSellersDataManager connects to the internet to retrieve data from the New York Times API. This means this isn’t really a unit test, this is an integration test.
So, okay, let’s mock the BestSellersDataManager. The problem is that…well…you can’t. Not without dipping into some more advanced features that come with a whole host of their own considerations that could make testing harder. This is because the instance BestSellersDataManager is created inside of the same method that uses it. Mocking only allows you to control the inputs to the method and how they behave.
The solution? Dependency Injection. Simply take the creation BestSellersDataManager out of the function, and create it somewhere else. In this case, we create an injectable-constructor that populations a field.
public class BestSellersService {
private final BestSellersDataManager bestSellersDataManager;
public BestSellersService() { //default constructor
this(new BestSellersDataManager());
}
protected BestSellersService(BestSellersDataManager bestSellersDataManager) { //injectable constructor
this.bestSellersDataManager = bestSellersDataManager;
}
public Book getLongestCurrentBestSeller(ListName listName) {
var bestSellers = bestSellersDataManager.getCurrentBestSellerList(listName);
var longestCurrentBook = bestSellers.getBooks().stream()
.max(Comparator.comparing(Book::getWeeksOnList));
if (longestCurrentBook.isEmpty()) {
throw new EmptyBestSellerListException(bestSellers.toString());
}
return longestCurrentBook.get();
}
}
Now, in our test, we can inject our mock bestSellersDataManager.
@ExtendWith(MockitoExtension.class)
public class BestSellersServiceTest {
private BestSellersService bestSellersService;
@Mock
private BestSellersDataManager mockDataManager;
@Mock
private BestSellersList mockBestSellersList;
@BeforeEach
public void setup() { //Dependency Injection here
bestSellersService = new BestSellersService(mockDataManager);
}
@Test
void getLongestCurrentBestSeller() {
Book gardensOfTheMoon = new Book("553812173","Gardens Of The Moon","Steve Erickson", 10); //10 weeks on list
Book deadhouseGates = new Book("0553813110","Deadhouse Gates","Steve Erickson", 5); //5 weeks on list
when(mockDataManager.getCurrentBestSellerList(ListName.COMBINED_FICTION)).thenReturn(mockBestSellersList);
when(mockBestSellersList.getBooks()).thenReturn(List.of(gardensOfTheMoon, deadhouseGates));
assertEquals(gardensOfTheMoon, bestSellersService.getLongestCurrentBestSeller(ListName.COMBINED_FICTION));
}
}
Dependency injection is necessary for us to be able to mock the behavior of the BestSellersDataManager to allow us to force the method to return testable data without actually using the New York Times API. In short, we have created a real unit-test that only depends on the logic in our BestSellersService class and our Book data structure.
Dependency injection is a powerful design tool for a number of reasons, but it is also a powerful testing tool as we see here, especially when we use mockito!