{"id":"unit-testing-good-tests","slug":"good-tests","title":"More assertions and good tests","text":"The tests so far compared numbers with `assertEquals`. JUnit has more assertions, and a few habits make tests a lot more useful.\n\n### assertTrue and assertFalse\n\n`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:\n\n```java\nassertTrue(garage.park());\nassertFalse(fullGarage.park());\n```\n\n`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.*;`.\n\n`assertEquals` works for Strings and other objects too. It compares them with `equals`, so two different String objects with the same letters are equal.\n\n### Comparing doubles\n\nAs 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.\n\n```java\nassertEquals(33.33, garage.percentFull(), 0.01);\n```\n\nThis 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.\n\nWhat 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.\n\nHere are all three in one test class. This Garage has `<=` in `park` where it should have `<`, a mistake right at the limit:\n\n```java\n// ==== Garage.java ====\npublic class Garage {\n    private int spots;\n    private int parked;\n\n    public Garage(int spots) {\n        this.spots = spots;\n        this.parked = 0;\n    }\n\n    public boolean park() {\n        if (this.parked <= this.spots) {\n            this.parked++;\n            return true;\n        }\n        return false;\n    }\n\n    public int freeSpots() {\n        return this.spots - this.parked;\n    }\n\n    public double percentFull() {\n        return 100.0 * this.parked / this.spots;\n    }\n}\n// ==== GarageTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class GarageTest {\n    @Test\n    public void carGetsASpotInAnEmptyGarage() {\n        Garage garage = new Garage(2);\n        assertTrue(garage.park());\n    }\n\n    @Test\n    public void lastFreeSpotCanBeTaken() {\n        Garage garage = new Garage(2);\n        garage.park();\n        assertTrue(garage.park());\n    }\n\n    @Test\n    public void fullGarageSaysNo() {\n        Garage garage = new Garage(2);\n        garage.park();\n        garage.park();\n        assertFalse(garage.park());\n    }\n\n    @Test\n    public void oneCarInThreeSpotsIsAThirdFull() {\n        Garage garage = new Garage(3);\n        garage.park();\n        assertEquals(33.33, garage.percentFull(), 0.01);\n    }\n}\n```\n\n```output\nGarageTest: 4 tests\n  carGetsASpotInAnEmptyGarage: passed\n  fullGarageSaysNo: FAILED\n    java.lang.AssertionError\n    at GarageTest.fullGarageSaysNo(GarageTest.java:24)\n  lastFreeSpotCanBeTaken: passed\n  oneCarInThreeSpotsIsAThirdFull: passed\n3 of 4 tests passed\n```\n\nWhen `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.\n\nNotice 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.\n\n### Which tests to write\n\nModule 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:\n\n- nothing at all: a new object, an empty list, a Scanner reading `\"\"`;\n- exactly one of something;\n- zero, and negative numbers;\n- exactly at a limit, and one past it.\n\nBugs gather at the edges, because that's where `<` or `<=`, or a loop's first and last round, make a difference.\n\n### Good tests\n\n- **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.\n- **A name that says what it checks**, like `fullGarageSaysNo`, not `test3`. When it fails, the name alone tells you what's broken.\n- **Work out the expected value yourself.** Copying what the code prints into the test only checks that the code does what it does.\n- **Each test makes its own objects**, so it doesn't depend on what the other tests did.\n","task":"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.\n","kind":"code","seed":"// ==== UsernameTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class UsernameTest {\n    @Test\n    public void ordinaryNameIsValid() {\n        assertTrue(new Username(\"robin\").isValid());\n    }\n}\n\n// ==== Username.java ====\npublic class Username {\n    private String name;\n\n    public Username(String name) {\n        this.name = name;\n    }\n\n    public boolean isValid() {\n        if (this.name.length() < 3 || this.name.length() > 12) {\n            return false;\n        }\n        return !this.name.contains(\" \");\n    }\n}\n","solution":"// ==== UsernameTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class UsernameTest {\n    @Test\n    public void ordinaryNameIsValid() {\n        assertTrue(new Username(\"robin\").isValid());\n    }\n\n    @Test\n    public void threeCharactersIsLongEnough() {\n        assertTrue(new Username(\"ada\").isValid());\n    }\n\n    @Test\n    public void twoCharactersIsTooShort() {\n        assertFalse(new Username(\"al\").isValid());\n    }\n\n    @Test\n    public void twelveCharactersIsShortEnough() {\n        assertTrue(new Username(\"maximilianus\").isValid());\n    }\n\n    @Test\n    public void thirteenCharactersIsTooLong() {\n        assertFalse(new Username(\"maximilianus2\").isValid());\n    }\n\n    @Test\n    public void nameWithASpaceIsInvalid() {\n        assertFalse(new Username(\"ada lovelace\").isValid());\n    }\n}\n\n// ==== Username.java ====\npublic class Username {\n    private String name;\n\n    public Username(String name) {\n        this.name = name;\n    }\n\n    public boolean isValid() {\n        if (this.name.length() < 3 || this.name.length() > 12) {\n            return false;\n        }\n        return !this.name.contains(\" \");\n    }\n}\n","hints":["Test each limit from both sides: a name of exactly 3 characters (valid) and one of 2 (too short), a name of exactly 12 (valid) and one of 13 (too long). Then a name with a space in it.","For example `assertTrue(new Username(\"ada\").isValid());` and `assertFalse(new Username(\"al\").isValid());`, each in a test of its own, with a name such as `threeCharactersIsLongEnough`. Count the characters of your long names carefully."],"tests":[{"name":"The Username as it is","stdin":"","junit":"UsernameTest","outcome":"pass","expect":"","hidden":false},{"name":"A Username that turns away names of exactly 3 characters","stdin":"","junit":"UsernameTest","outcome":"fail","replace":{"Username.java":"public class Username {\n    private String name;\n\n    public Username(String name) {\n        this.name = name;\n    }\n\n    public boolean isValid() {\n        if (this.name.length() <= 3 || this.name.length() > 12) {\n            return false;\n        }\n        return !this.name.contains(\" \");\n    }\n}\n"},"expect":"","hidden":false},{"name":"A Username that is wrong at the other limit","stdin":"","junit":"UsernameTest","outcome":"fail","replace":{"Username.java":"public class Username {\n    private String name;\n\n    public Username(String name) {\n        this.name = name;\n    }\n\n    public boolean isValid() {\n        if (this.name.length() < 3 || this.name.length() >= 12) {\n            return false;\n        }\n        return !this.name.contains(\" \");\n    }\n}\n"},"expect":"","hidden":true},{"name":"A Username that forgets one of the rules","stdin":"","junit":"UsernameTest","outcome":"fail","replace":{"Username.java":"public class Username {\n    private String name;\n\n    public Username(String name) {\n        this.name = name;\n    }\n\n    public boolean isValid() {\n        if (this.name.length() < 3 || this.name.length() > 12) {\n            return false;\n        }\n        return true;\n    }\n}\n"},"expect":"","hidden":true}],"require":[],"forbid":[],"more":[{"task":"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.\n\nWrite 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)`.\n","kind":"code","seed":"// ==== ReviewsTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class ReviewsTest {\n    @Test\n    public void oneReviewIsItsOwnAverage() {\n        Reviews reviews = new Reviews();\n        reviews.add(4);\n        assertEquals(4.0, reviews.average(), 0.001);\n    }\n}\n\n// ==== Reviews.java ====\nimport java.util.ArrayList;\n\npublic class Reviews {\n    private ArrayList<Integer> stars;\n\n    public Reviews() {\n        this.stars = new ArrayList<>();\n    }\n\n    public void add(int stars) {\n        if (stars >= 1 && stars <= 5) {\n            this.stars.add(stars);\n        }\n    }\n\n    public int count() {\n        return this.stars.size();\n    }\n\n    public double average() {\n        if (this.stars.isEmpty()) {\n            return 0.0;\n        }\n        int sum = 0;\n        for (int star : this.stars) {\n            sum += star;\n        }\n        return (double) sum / this.stars.size();\n    }\n}\n","solution":"// ==== ReviewsTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class ReviewsTest {\n    @Test\n    public void oneReviewIsItsOwnAverage() {\n        Reviews reviews = new Reviews();\n        reviews.add(4);\n        assertEquals(4.0, reviews.average(), 0.001);\n    }\n\n    @Test\n    public void averageKeepsTheDecimals() {\n        Reviews reviews = new Reviews();\n        reviews.add(4);\n        reviews.add(5);\n        assertEquals(4.5, reviews.average(), 0.001);\n    }\n\n    @Test\n    public void noReviewsAverageZero() {\n        Reviews reviews = new Reviews();\n        assertEquals(0.0, reviews.average(), 0.001);\n    }\n\n    @Test\n    public void starsOutsideOneToFiveAreIgnored() {\n        Reviews reviews = new Reviews();\n        reviews.add(0);\n        reviews.add(6);\n        reviews.add(3);\n        assertEquals(1, reviews.count());\n        assertEquals(3.0, reviews.average(), 0.001);\n    }\n}\n\n// ==== Reviews.java ====\nimport java.util.ArrayList;\n\npublic class Reviews {\n    private ArrayList<Integer> stars;\n\n    public Reviews() {\n        this.stars = new ArrayList<>();\n    }\n\n    public void add(int stars) {\n        if (stars >= 1 && stars <= 5) {\n            this.stars.add(stars);\n        }\n    }\n\n    public int count() {\n        return this.stars.size();\n    }\n\n    public double average() {\n        if (this.stars.isEmpty()) {\n            return 0.0;\n        }\n        int sum = 0;\n        for (int star : this.stars) {\n            sum += star;\n        }\n        return (double) sum / this.stars.size();\n    }\n}\n","hints":["One test with stars whose average isn't a whole number, like 4 and 5. One with no reviews at all. And one that adds stars outside 1 to 5, such as 0 and 6, next to a normal one, and checks `count()` or the average.","For example: `reviews.add(4); reviews.add(5);` and then `assertEquals(4.5, reviews.average(), 0.001);`. With no reviews, `assertEquals(0.0, reviews.average(), 0.001);`."],"tests":[{"name":"The Reviews as they are","stdin":"","junit":"ReviewsTest","outcome":"pass","expect":"","hidden":false},{"name":"Reviews whose average drops the decimals","stdin":"","junit":"ReviewsTest","outcome":"fail","replace":{"Reviews.java":"import java.util.ArrayList;\n\npublic class Reviews {\n    private ArrayList<Integer> stars;\n\n    public Reviews() {\n        this.stars = new ArrayList<>();\n    }\n\n    public void add(int stars) {\n        if (stars >= 1 && stars <= 5) {\n            this.stars.add(stars);\n        }\n    }\n\n    public int count() {\n        return this.stars.size();\n    }\n\n    public double average() {\n        if (this.stars.isEmpty()) {\n            return 0.0;\n        }\n        int sum = 0;\n        for (int star : this.stars) {\n            sum += star;\n        }\n        return sum / this.stars.size();\n    }\n}\n"},"expect":"","hidden":false},{"name":"Reviews with a bug when there are none","stdin":"","junit":"ReviewsTest","outcome":"fail","replace":{"Reviews.java":"import java.util.ArrayList;\n\npublic class Reviews {\n    private ArrayList<Integer> stars;\n\n    public Reviews() {\n        this.stars = new ArrayList<>();\n    }\n\n    public void add(int stars) {\n        if (stars >= 1 && stars <= 5) {\n            this.stars.add(stars);\n        }\n    }\n\n    public int count() {\n        return this.stars.size();\n    }\n\n    public double average() {\n        int sum = 0;\n        for (int star : this.stars) {\n            sum += star;\n        }\n        return (double) sum / this.stars.size();\n    }\n}\n"},"expect":"","hidden":true},{"name":"Reviews with a bug in which stars they accept","stdin":"","junit":"ReviewsTest","outcome":"fail","replace":{"Reviews.java":"import java.util.ArrayList;\n\npublic class Reviews {\n    private ArrayList<Integer> stars;\n\n    public Reviews() {\n        this.stars = new ArrayList<>();\n    }\n\n    public void add(int stars) {\n        if (stars >= 0 && stars <= 5) {\n            this.stars.add(stars);\n        }\n    }\n\n    public int count() {\n        return this.stars.size();\n    }\n\n    public double average() {\n        if (this.stars.isEmpty()) {\n            return 0.0;\n        }\n        int sum = 0;\n        for (int star : this.stars) {\n            sum += star;\n        }\n        return (double) sum / this.stars.size();\n    }\n}\n"},"expect":"","hidden":true}],"require":[],"forbid":[]},{"task":"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.\n\nWrite 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.\n","kind":"code","seed":"// ==== SetlistTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class SetlistTest {\n    @Test\n    public void longestSongInTheMiddleIsFound() {\n        Setlist setlist = new Setlist();\n        setlist.add(new Song(\"Intro\", 95));\n        setlist.add(new Song(\"Long Road\", 410));\n        setlist.add(new Song(\"Encore\", 200));\n        assertEquals(\"Long Road\", setlist.longestSong());\n    }\n}\n\n// ==== Setlist.java ====\nimport java.util.ArrayList;\n\npublic class Setlist {\n    private ArrayList<Song> songs;\n\n    public Setlist() {\n        this.songs = new ArrayList<>();\n    }\n\n    public void add(Song song) {\n        this.songs.add(song);\n    }\n\n    public String longestSong() {\n        if (this.songs.isEmpty()) {\n            return \"\";\n        }\n        Song longest = this.songs.get(0);\n        for (int i = 1; i < this.songs.size(); i++) {\n            if (this.songs.get(i).getSeconds() > longest.getSeconds()) {\n                longest = this.songs.get(i);\n            }\n        }\n        return longest.getTitle();\n    }\n}\n\n// ==== Song.java ====\npublic class Song {\n    private String title;\n    private int seconds;\n\n    public Song(String title, int seconds) {\n        this.title = title;\n        this.seconds = seconds;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public int getSeconds() {\n        return this.seconds;\n    }\n}\n","solution":"// ==== SetlistTest.java ====\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\npublic class SetlistTest {\n    @Test\n    public void longestSongInTheMiddleIsFound() {\n        Setlist setlist = new Setlist();\n        setlist.add(new Song(\"Intro\", 95));\n        setlist.add(new Song(\"Long Road\", 410));\n        setlist.add(new Song(\"Encore\", 200));\n        assertEquals(\"Long Road\", setlist.longestSong());\n    }\n\n    @Test\n    public void longestSongAtTheEndIsFound() {\n        Setlist setlist = new Setlist();\n        setlist.add(new Song(\"Intro\", 95));\n        setlist.add(new Song(\"Encore\", 300));\n        assertEquals(\"Encore\", setlist.longestSong());\n    }\n\n    @Test\n    public void firstOfEquallyLongSongsWins() {\n        Setlist setlist = new Setlist();\n        setlist.add(new Song(\"Blue\", 180));\n        setlist.add(new Song(\"Green\", 180));\n        assertEquals(\"Blue\", setlist.longestSong());\n    }\n\n    @Test\n    public void emptySetlistGivesEmptyTitle() {\n        Setlist setlist = new Setlist();\n        assertEquals(\"\", setlist.longestSong());\n    }\n}\n\n// ==== Setlist.java ====\nimport java.util.ArrayList;\n\npublic class Setlist {\n    private ArrayList<Song> songs;\n\n    public Setlist() {\n        this.songs = new ArrayList<>();\n    }\n\n    public void add(Song song) {\n        this.songs.add(song);\n    }\n\n    public String longestSong() {\n        if (this.songs.isEmpty()) {\n            return \"\";\n        }\n        Song longest = this.songs.get(0);\n        for (int i = 1; i < this.songs.size(); i++) {\n            if (this.songs.get(i).getSeconds() > longest.getSeconds()) {\n                longest = this.songs.get(i);\n            }\n        }\n        return longest.getTitle();\n    }\n}\n\n// ==== Song.java ====\npublic class Song {\n    private String title;\n    private int seconds;\n\n    public Song(String title, int seconds) {\n        this.title = title;\n        this.seconds = seconds;\n    }\n\n    public String getTitle() {\n        return this.title;\n    }\n\n    public int getSeconds() {\n        return this.seconds;\n    }\n}\n","hints":["Write three new tests, each with a Setlist of its own: one where the last song is the longest, one with two songs of the same length, and one with no songs.","For the tie: `setlist.add(new Song(\"Blue\", 180));`, `setlist.add(new Song(\"Green\", 180));` and `assertEquals(\"Blue\", setlist.longestSong());`. For no songs: `assertEquals(\"\", setlist.longestSong());`."],"tests":[{"name":"The Setlist as it is","stdin":"","junit":"SetlistTest","outcome":"pass","expect":"","hidden":false},{"name":"A Setlist that never looks at the last song","stdin":"","junit":"SetlistTest","outcome":"fail","replace":{"Setlist.java":"import java.util.ArrayList;\n\npublic class Setlist {\n    private ArrayList<Song> songs;\n\n    public Setlist() {\n        this.songs = new ArrayList<>();\n    }\n\n    public void add(Song song) {\n        this.songs.add(song);\n    }\n\n    public String longestSong() {\n        if (this.songs.isEmpty()) {\n            return \"\";\n        }\n        Song longest = this.songs.get(0);\n        for (int i = 1; i < this.songs.size() - 1; i++) {\n            if (this.songs.get(i).getSeconds() > longest.getSeconds()) {\n                longest = this.songs.get(i);\n            }\n        }\n        return longest.getTitle();\n    }\n}\n"},"expect":"","hidden":false},{"name":"A Setlist with a bug when songs are equally long","stdin":"","junit":"SetlistTest","outcome":"fail","replace":{"Setlist.java":"import java.util.ArrayList;\n\npublic class Setlist {\n    private ArrayList<Song> songs;\n\n    public Setlist() {\n        this.songs = new ArrayList<>();\n    }\n\n    public void add(Song song) {\n        this.songs.add(song);\n    }\n\n    public String longestSong() {\n        if (this.songs.isEmpty()) {\n            return \"\";\n        }\n        Song longest = this.songs.get(0);\n        for (int i = 1; i < this.songs.size(); i++) {\n            if (this.songs.get(i).getSeconds() >= longest.getSeconds()) {\n                longest = this.songs.get(i);\n            }\n        }\n        return longest.getTitle();\n    }\n}\n"},"expect":"","hidden":true},{"name":"A Setlist with a bug when it has no songs","stdin":"","junit":"SetlistTest","outcome":"fail","replace":{"Setlist.java":"import java.util.ArrayList;\n\npublic class Setlist {\n    private ArrayList<Song> songs;\n\n    public Setlist() {\n        this.songs = new ArrayList<>();\n    }\n\n    public void add(Song song) {\n        this.songs.add(song);\n    }\n\n    public String longestSong() {\n        Song longest = this.songs.get(0);\n        for (int i = 1; i < this.songs.size(); i++) {\n            if (this.songs.get(i).getSeconds() > longest.getSeconds()) {\n                longest = this.songs.get(i);\n            }\n        }\n        return longest.getTitle();\n    }\n}\n"},"expect":"","hidden":true}],"require":[],"forbid":[]}]}