In this unit, we can show how mock objects can be used to help us more precisely test specific functions without testing or needing to use their dependencies directly. This can help us with testing higher-level code without also testing or setting up their low-level dependencies.
When doing unit testing, our goal is to test a single unit of code. Now, this isn’t new information. However, when developing a module, we can often connect a module into the rest of our system instinctively. This means that when testing, we aren’t actually doing unit testing, despite our best intentions.
Consider this test in NBATeamReaderIntegrationTest
@Test
public void getsThirtyTeams() {
NBATeamReader reader = new NBATeamReader();
List<NBATeam> teams = reader.getNBATeams();
assertEquals(30, teams.size());
}
Is this a unit test?
Well…no, actually. However, this may not be readily apparent by
simply looking at this test. Instead, we need to dig into the
getNBATeams()
function in NBATeamReader
Specifically, the function getNBATeams()
calls the function
getTeamsFromAPI()
, which in turn calls the function getTeamsArrayFromAPI()
.
Here is that function:
private JSONArray getTeamsArrayFromAPI() {
BallDontLieReader apiReader = new BallDontLieReader();
JSONObject apiOutput = apiReader.getAllNBATeams();
return apiOutput.getJSONArray("data");
}
Here, you will see that this function has a clear external
dependency to the class BallDontLieReader
. This means
by calling getNBATeams()
in our test, we are testing not
just if the function getNBATeams()
works. Necessarily,
we are also reliant on the class BallDontLieReader
working.
This means our test getThirtyTeams()
is not a Unit Test
for the following reasons:
getAllNBATeams()
in the class BallDontLieReader
. This means
if either function has a defect, both tests will fail.BallDontLieReader.getAllNBATeams()
could fail for reasons
that have nothing to do with code being test!
With this in mind, even BallDontLieReaderTest
is full of tests which are not unit tests. We have an external dependency to a web service API, and a failure in that communication would cause our tests to fail even if our code is functionally correct.
When unit testing, we only want to test the class we are testing! We do not want our tests to fail for any other reason, of our tests could be considered misleading, and people could ignore failures, attributing them to external dependency issues. Note that dealing with this in a class directly interacting with external information can be difficult. However, in a class like NBATeamReader, which only interacts with other classes in the program, we should be able isolate the class for testing with external dependencies.
Consider this classic scene from the TV Show the Simpsons:
In the scene, Lisa notices that Homer’s “Check Engine” light is on. Homer says “Uh-oh, tape must have fallen off.” Homer then puts a piece of electric tape over the light to block it out, and says “There, problem solved!” Humorously, the care immediately breaks down due to an engine issue.
We do not want a failing unit test to be a check engine light! Ideally, a unit test should fail if and only if the code being tested has a defect. We want to avoid false positives (that is, a failing test indicating a defect when there is none), because if we allow too many, you and your fellow developers will begin to think that a failed test “probably isn’t your fault”, and that way madness lies.
To be clear, integration tests are still valuable. We shouldn’t
delete the existing tests we have. However, integration tests are
not a substitute for effective unit tests. So, without deleting
these existing tests, lets work on writing Unit tests for classes that use BallDontLieReader
and NBATeamReader
, while severing the reliance on any external dependencies.
If you wish to disable a test from automated testing because it relies on external dependencies, like a Web-API, you can use the @Disabled(String message)
tag on the test method. The Disabled message will prevent the test from running automatically with the rest of the tests. You can also break tests up into groups to run separately, but that’s beyond the scope of this course.
Of course, the obvious question is “how do we test an API reader without an API”? The answer is *Test Doubles.
Test Doubles are classes that replace external dependency objects with replacement objects that imitate the behavior of the external dependency they are replacing. In this case, an external dependency is any code used by a class that isn’t part of the class.
A Stub object is an object that replaces an object with another object of the same interface. However, the Stub class’s implementation is typically hardcoded to return specific values. For example, if we were to use the Stub of NBATeamReader
, we may write something like this:
public class StubNBATeamReader extends NBATeamReader {
public static final NBATeam LAKERS = new NBATeam(1,"Lakers","Los Angelos","LAL",
Conference.WESTERN, Division.PACIFIC);
public static final NBATeam CELTICS = new NBATeam(2, "Celtics", "Boston", "BOS",
Conference.EASTERN, Division.ATLANTIC);
public List<NBATeam> getNBATeams() {
return generateFakeNBATeamList();
}
private List<NBATeam> generateFakeNBATeamList() {
return List.of(LAKERS, CELTICS);
}
}
This code is in the class StubNBATeamReader
. Please be aware
that the List.of
function was introduced in Java 9, and will not
work in earlier versions of Java.
We would save this class in our test folder, not in our
src folder, as we only use this class while testing.
Then, if we were testing a function that used the class NBATeamReader
, we would simply use an instance of StubNBATeamReader
instead.
For instance, say we wanted to test the class GoodAbbreviations
, specifically the method extractGoodAbbreviationTeams(NBATeamReader nbaTeamReader)
. If we followed the pattern of our earlier tests in this project, we would test this by creating an NBATeamReader
object that interacts with BallDontLieReader
that interacts with the external Web API. However, that’s not a unit test! Instead, let’s test using our fake class.
A reminder that in our earlier unit on Poi, we described a
“good abbreviation” as one where the first three letters of the
team’s city matches the team’s name. As such, our output from
getNBATeams()
in StubNBATeamReader
is perfect for this test,
as it gives us one team with a good abbreviation (Boston Celtics)
and on team without a good abbreviation (LA Lakers). Thus, we know
our input (our StubNBATeamReader
), and are expected output (a list containing only the Boston Celtics).
Below is our GoodAbbreviationsTest
class.
package edu.virginia.cs.nbateams;
import org.junit.jupiter.api.*;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
public class GoodAbbreviationsTest {
@Test
public void testGoodAbbreviations() {
GoodAbbreviations abbreviations = new GoodAbbreviations();
NBATeamReader reader = new StubNBATeamReader();
List<NBATeam> goodAbbreviationsTeams = abbreviations.extractGoodAbbreviationTeams(reader);
List<NBATeam> expected = List.of(StubNBATeamReader.CELTICS);
assertEquals(expected, goodAbbreviationsTeams);
}
}
Using the above, we are able to test the class GoodAbbreviations
without needing to rely on any external dependencies. The class StubNBATeamReader
is used instead via polymorphism (since StubNBATeamReader
extends our real class NBATeamReader
). Because the result is hard-coded, it allows us to return a value that is tailor-made to test our method (in this case, we test both the case of a team with a “good” Abbreviation and a team without one).
One limitation of this stub is that we only designed it for testing one method. What if we wanted to test a method for team’s whose “city” value is actually a state, like the Utah Jazz or Minnesota Timberwolves? Now this stub isn’t as flexible. We have two options in order to maintain this class:
We go back and change our existing Stub class every time we want to test a new method. This approach is bad because every time we change our existing Stub class, any existing tests might have to change. For example, adding Utah Jazz to our stub class to test the state name would require us to update our GoodAbbreviationsTest
Create a new stub class for each test. But this means creating a lot of classes that are only used in a single class, polluting the global name space.
What we need is a way to create a Stub class without actually creating a class. We will cover that in the next unit with mockito.