Mockito

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.



Mockito usage example

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);
    }
}

Stub Test Double

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.

Writing a stub with mockito

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

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.

Use Mockito 4, not Mockito 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 statements

import static org.mockito.Mockito.*;

This gives you access to the functions in mockito we want to use.

Top-down integration

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.

Example

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).

Wait, what about JSON?

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:

  • “professor” is the key to a map that has keys “name” and “degrees”
    • “name” is the key to a map that has keys:
      • “first” - value “Paul”
      • “last” - value “McBurney”
      • “preferred” - value “Will”
    • “degrees is the key to a list of degrees:
      • index 0 is my Bachelor’s degree
      • index 1 is my Master’s degree
      • index 2 is my Ph.D.

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.

Mocks

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);
    }
}

Mockito verify function

verify 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.

Purpose of 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 methods

When 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.

Why use test Doubles

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.


Previous submodule:
Next submodule: