Skip to main content

Section 8.2 Unit Tests

A unit test asserts that a specific class achieves a very specific functionality. If the test fails it should be because you have not implemented that functionality appropriately, and for no other reason.
Unit tests always concern themselves with a unit under test, often abbreviated UUT. This is the specific object and method whose functionality we aim to verify. These tests are named in a way that reflects what they verify, and are typically anywhere from 1 to 10 lines, ideally. We will talk more about this later, but one thing is important:
Unit tests need to be clear and readable. You should look at a test code and have no doubt whatsoever that it is a correct test of the desired functionality.
A common acronym when referring to desirable test properties is F.I.R.S.T. We want tests to be:
  • Fast, running some fraction of a second, ideally in milliseconds. You want to be able to run your tests the moment you make small code changes, and receive timely feedback.
  • Isolated. If a certain functionality is not implemented correctly, ideally only one test should be failing, the one that was supposed to test this functionality. The letter I also stands for Independent, meaning that each test should in no way depend on other tests to be done first, before it can run. I should be able to run the tests in any order, possibly omit some or most of the tests, without any problems.
  • Repeatable, meaning that I can run the tests multiple times with predictable results, and that I can run them in any environment and expect the same results.
  • Self-validating, not requiring manual inspection to determine if they have failed or succeeded.
  • Timely, written just before the code that they are made to test. We will discuss the mechanics and importance of this in the next section.
Let’s take a look at some of the tests for the grade-reporting program we have been writing, here is one of them:
@Test
public void multipleCoursesAreAllComputed() {
  assertTotalsOfList_Are(
    "CS 234        A \nMAT 111      B",
    "Courses: 2\nGPA: 3.50\n");
  assertTotalsOfList_Are(
    "CS 234        A \nMAT 111      B\nCS 122     B-\n",
    "Courses: 3\nGPA: 3.22\n");
}

private void assertTotalsOfList_Are(String input, String output) {
  assertEquals(output, processGrades(new Scanner(input)));
}
The method with the @Testannotation above it is a test method, using the JUnit framework. It calls on a helper method called assertTotalsOfList_Are which takes an input string, processes it via the processGrades method, then compares the result to the output via the method assertEquals. Testing frameworks traditionally provide such methods for you, and JUnit is no exception. assertEquals terminates normally if its two parameters are equal, otherwise it throws an exception, which the testing framework reports back to us.
So this one test method is supposed to test that if the file contains multiple rows of grades, they will all be used in the calculation. So for example if the input is:
CS 234        A
MAT 111      B
then the method expects to see the result
Courses: 2
GPA: 3.50
Now let’s discuss how this holds up against the FIRST acronym. This test is certainly fast, it will take milliseconds to run. It doesn’t try to process millions of lines or download something from the web.
Next, it is independent. I can run this test alone and it will run just fine. It does not depend on some database, or for a file to exist some place, or for other tests to do their thing first, it is completely self-contained.
Unfortunately it is not so isolated. Here’s why: Suppose this test fails. Did it fail because we didn’t read the grade letter properly from the input? Or is it because we didn’t translate letter grades to points properly? Or perhaps the problem was in our process for computing the total points and number of courses? Or did we have a formatting error when printing the output? Any one of those very diverse reasons could have caused the problem! And now that the code is in 4 separate files, we don’t even know which file to look at to find the problem! Ideally we should have had more "localized" tests: some tests for the GradeReader class to make sure letters are read properly, some for the Grade class to make sure grades are represented accurately, some for the summary class to make sure summaries work correctly, and so on. What we have is a decent integration test, that shows all those pieces working well together. But it is not a very isolated unit test.
It is repeatable. I can run it over and over again and it won’t cause any problems. It’s not creating files, it’s not changing the database. What’s more, my teammates can run this test, and you all can run it after you download my code, it doesn’t depend on anything that’s not there already.
It is self-validating. If the test succeeds you don’t need to do anything, you just see a green light. If it fails, you’ll see all sorts of red marks popping up with details on what went wrong. A non-self-validating test would have printed the result string, rather than doing assertEquals, and expect us to visually compare that string to the expected result to see if it failed or not.
The test is timely. We wrote that test before we wrote the corresponding code. It wasn’t an afterthought. But we’ll come back to these ideas in the next chapter.