Java Arena
Step 4 of 6

More assertions and good tests

The tests so far compared numbers with assertEquals. JUnit has more assertions, and a few habits make tests a lot more useful.

assertTrue and assertFalse

park() returns true when the car got a spot, and false when the garage was full. For a method that returns a boolean, assertTrue(...) passes when the value is true, and assertFalse(...) when it's false:

assertTrue(garage.park());
assertFalse(fullGarage.park());

assertTrue(garage.park()) passes and fails in exactly the same cases as assertEquals(true, garage.park()), and reads better. Only the message on a failure differs: assertEquals says expected:<true> but was:<false>, while assertTrue says just java.lang.AssertionError, as you'll see below. To use all of JUnit's assertions without importing each one by name, write import static org.junit.Assert.*;.

assertEquals works for Strings and other objects too. It compares them with equals, so two different String objects with the same letters are equal.

Comparing doubles

As module 18 showed, a double is often a tiny bit off: 0.1 + 0.2 is 0.30000000000000004. And a garage with one car in three spots is 33.333333333333336% full, which nobody wants to type into a test. So for doubles, assertEquals takes a third value, the delta: how far apart the two values may be and still count as equal.

assertEquals(33.33, garage.percentFull(), 0.01);

This passes when percentFull() returns anything within 0.01 of 33.33. Choose a delta smaller than any difference that would matter: for a percentage, 0.01 is plenty.

What about assertEquals(33.33, garage.percentFull()), without the delta? Such a call would compare the two doubles exactly, and pass or fail by the luck of the last digits. So JUnit has a version of assertEquals for two doubles that is there to stop this mistake: it fails the test every time, with the message Use assertEquals(expected, actual, delta) to compare floating-point numbers. It's marked deprecated, which means "don't use this", so javac also prints a note that the code uses something deprecated.

Here are all three in one test class. This Garage has <= in park where it should have <, a mistake right at the limit:

Garage.java
public class Garage {
    private int spots;
    private int parked;

    public Garage(int spots) {
        this.spots = spots;
        this.parked = 0;
    }

    public boolean park() {
        if (this.parked <= this.spots) {
            this.parked++;
            return true;
        }
        return false;
    }

    public int freeSpots() {
        return this.spots - this.parked;
    }

    public double percentFull() {
        return 100.0 * this.parked / this.spots;
    }
}
GarageTest.java
import static org.junit.Assert.*;

import org.junit.Test;

public class GarageTest {
    @Test
    public void carGetsASpotInAnEmptyGarage() {
        Garage garage = new Garage(2);
        assertTrue(garage.park());
    }

    @Test
    public void lastFreeSpotCanBeTaken() {
        Garage garage = new Garage(2);
        garage.park();
        assertTrue(garage.park());
    }

    @Test
    public void fullGarageSaysNo() {
        Garage garage = new Garage(2);
        garage.park();
        garage.park();
        assertFalse(garage.park());
    }

    @Test
    public void oneCarInThreeSpotsIsAThirdFull() {
        Garage garage = new Garage(3);
        garage.park();
        assertEquals(33.33, garage.percentFull(), 0.01);
    }
}
Output
GarageTest: 4 tests
  carGetsASpotInAnEmptyGarage: passed
  fullGarageSaysNo: FAILED
    java.lang.AssertionError
    at GarageTest.fullGarageSaysNo(GarageTest.java:24)
  lastFreeSpotCanBeTaken: passed
  oneCarInThreeSpotsIsAThirdFull: passed
3 of 4 tests passed

When assertTrue or assertFalse fails, the report says only java.lang.AssertionError: there's no expected and actual value to show. So the test's name has to tell the story, and here it does: a full garage should say no, and it didn't.

Notice the two tests at the limit. lastFreeSpotCanBeTaken checks that the last spot can be used, and fullGarageSaysNo checks that the car after it is turned away. Together they pin the limit down exactly. With <, as it should be, both pass. With <=, a full garage takes one car too many, and fullGarageSaysNo fails. If the limit were off the other way, say this.parked < this.spots - 1, the last spot would be refused, and lastFreeSpotCanBeTaken would fail.

Which tests to write

Module 12 listed the inputs worth trying: a typical case, the edges, and the unusual. With tests, every case you think of becomes a test that runs each time from then on. For a class, the edges are often:

  • nothing at all: a new object, an empty list, a Scanner reading "";
  • exactly one of something;
  • zero, and negative numbers;
  • exactly at a limit, and one past it.

Bugs gather at the edges, because that's where < or <=, or a loop's first and last round, make a difference.

Good tests

  • One idea per test. A test stops at its first failed assertion, so a test that checks five things hides the other four when one fails. Small tests each report on their own.
  • A name that says what it checks, like fullGarageSaysNo, not test3. When it fails, the name alone tells you what's broken.
  • Work out the expected value yourself. Copying what the code prints into the test only checks that the code does what it does.
  • Each test makes its own objects, so it doesn't depend on what the other tests did.

Your task

Challenge 1 of 3

A sign-up form accepts a username of 3 to 12 characters, with no spaces, and Username checks it with isValid(). UsernameTest has one test, for an ordinary name. Write more tests with assertTrue and assertFalse, so they catch a mistake at either limit, and a forgotten rule. The checks run your tests on the Username as it is, where all of them should pass, and on three versions with a bug, where at least one should fail. One of them is shown: a Username that turns away names of exactly 3 characters.

Challenge 2

Reviews on a recipe site give 1 to 5 stars, and anything else is ignored. average() returns the average of the stars, and 0.0 when there are none. ReviewsTest has one test, which passes even on the broken versions.

Write tests that catch a version whose average drops the decimals (giving 4.0 for the stars 4 and 5, instead of 4.5), and two hidden versions with other bugs. Think about the edges: no reviews at all, and stars just outside 1 to 5, 0 and 6. Compare doubles with a delta, as in assertEquals(4.5, reviews.average(), 0.001).

Challenge 3

A band's setlist holds its songs, and each song knows its title and length in seconds. longestSong() returns the title of the longest song. If several are equally long, it returns the first of them, and with no songs it returns an empty String. The test in SetlistTest finds a longest song in the middle of the list, which even buggy versions get right.

Write tests for the edges: the longest song at the end of the list, two songs that are equally long, and a setlist with no songs. assertEquals compares Strings too. The checks run your tests on the Setlist as it is and on three versions with a bug; one of them is shown: a Setlist that never looks at the last song.