Lesson 22Phase 2: Objects
Testing your code with JUnit
Writing small automatic checks that prove a class works, and running them every time the code changes.
By BasithUpdated 55 min
- unit test
- JUnit
- @Test
- assertEquals
- assertThrows
- arrange, act, assert
Until now you have checked every class the same way: run main, read what it prints, and decide in your head whether the numbers look right. That works on the day you write the code. This lesson moves the checking into code. You will write small tests that know the right answers, run all of them in a second, and run them again after every change, so a mistake shows up minutes after you make it instead of months later.
- Explain what a unit test is, and why it beats printing results and checking them by eye.
- Write a JUnit test class with
@Testmethods, run it, and read a failure: what the test expected and what the code gave. - Give every test the arrange, act, assert shape, one behaviour per test, and a name that says what should happen.
- Choose between
assertEquals(with a delta for doubles),assertTrue,assertFalse,assertNullandassertThrows. - Hunt for edge cases, keep tests independent with
@BeforeEach, and run one test over many inputs. - Build a method test first with red, green, refactor, and run tests in an editor, with
mvn testand withgradle test.
BankAccount refused a negative deposit by printing a message; this page gives it a better way to say no. You have used one annotation, @Override from overriding and super, a label that tells Java what you meant. JUnit runs on labels like that. From strings and string practice you have charAt loops, StringBuilder and a plan for a word counter that counts the spaces and adds one, and a test on this page finds the flaw in that plan. And reading errors and debugging named the bug where a program runs but gives the wrong answer: a logic error. Tests catch those early.Checking by eye does not last#
Here is the BankAccount from the encapsulation lesson, cut down to whole amounts so the sums stay easy. The usual way to check a class like this is a main that calls a few methods and prints the results.
public class Main { public static void main(String[] args) { BankAccount account = new BankAccount(100); account.deposit(50); System.out.println("After deposit: " + account.getBalance()); account.withdraw(30); System.out.println("After withdraw: " + account.getBalance()); }} class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { balance = balance + amount; } void withdraw(int amount) { balance = balance - amount; } int getBalance() { return balance; }}It prints After deposit: 150 and After withdraw: 120. To decide whether that is right, you work the sums out yourself: 100 plus 50 is 150, and 150 minus 30 is 120. The program printed numbers, and you did the checking.
That is fine today. Next month you add a fee to withdraw, or someone tidies up deposit. Is the class still right? To find out you would run main again, read every line and redo every sum, for every method you ever checked. Nobody does that after every change, so a mistake can sit in the code for weeks before anyone notices a wrong number.
Write the right answer into the code
The fix is to write the answer you expect into the program, next to the call, and let the computer compare the two. Then the program can say for itself whether it passed.
public class Main { public static void main(String[] args) { BankAccount account = new BankAccount(100); account.deposit(50); if (account.getBalance() == 150) { System.out.println("PASS: deposit"); } else { System.out.println("FAIL: deposit gave " + account.getBalance()); } account.withdraw(30); if (account.getBalance() == 120) { System.out.println("PASS: withdraw"); } else { System.out.println("FAIL: withdraw gave " + account.getBalance()); } }} class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { balance = balance + amount; } void withdraw(int amount) { balance = balance - amount; } int getBalance() { return balance; }}This prints PASS: deposit and PASS: withdraw. The fact that the balance should be 150 now lives in the code, so the check can run again tomorrow, or next year, without you. Code like this, which runs a piece of a program and checks the result itself, is a test. A test of one small piece, usually one method of one class, is a unit test.
Hand-made checks soon grow tedious, though. Each one needs its own if, else and message. Both checks share one account, so if the deposit went wrong, the withdraw check would start from a wrong balance and fail too, even with nothing wrong in withdraw. And you still have to read the output to spot the word FAIL. A testing library takes care of all of that.
Key ideaA unit test runs one small piece of your code and checks the answer itself. The expected answer is written down, so every check can run again in seconds after any change.
Your first JUnit test#
JUnit is the testing library most Java projects use. A library is code that someone else wrote and you add to your project. JUnit does not come with the JDK, and the last section of this page shows how a real project adds it. In this course the engine has JUnit built in, so a test class runs with the Run button like any other program.
Here is the same BankAccount with a test class under it. There is no main. Press Run, then step through and watch the engine make a new BankAccountTest object for each test.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { balance = balance + amount; } void withdraw(int amount) { balance = balance - amount; } int getBalance() { return balance; }} class BankAccountTest { @Test void depositAddsToTheBalance() { BankAccount account = new BankAccount(100); account.deposit(50); assertEquals(150, account.getBalance()); } @Test void withdrawTakesFromTheBalance() { BankAccount account = new BankAccount(100); account.withdraw(30); assertEquals(70, account.getBalance()); }}The console shows a report: the name of the test class, one line per test with a tick (✔) when it passed, and a count at the end, 2 tests: 2 passed, 0 failed.
Reading the test class
import org.junit.jupiter.api.Test;brings in JUnit's@Test, the wayimport java.util.ArrayList;brought inArrayList.org.junit.jupiter.apiis the package JUnit keeps it in.import static org.junit.jupiter.api.Assertions.assertEquals;is a static import. It lets you writeassertEquals(...)where you would otherwise needAssertions.assertEquals(...). The wordstaticgets a full explanation in static and final; here it only means “let me use this method by its short name”.class BankAccountTestis an ordinary class. By convention a test class is named after the class it tests, withTeston the end.@Testmarks a method as a test. Like@Override, it is an annotation, a label on the code. JUnit looks for methods with this label and runs each one.- Each test method is
voidand takes no parameters, because JUnit calls it with nothing to pass in and nothing to take back. Later on this page you will meet tests that do take a parameter. assertEquals(150, account.getBalance())is the check. The first argument is the value you expect, and the second is the value the code actually produced. If they are equal, the line does nothing and the test carries on. If they differ, the test stops on that line and counts as failed.
main. A test runner, a program that comes with JUnit, finds the classes that have @Test methods. For each test it makes a new object of the test class and calls the test method on it. In this course the Run button is the test runner. In a real project, your editor or your build tool is.When a test fails
A test earns its keep on the day it fails. Break the class on purpose: in withdraw, change the minus into a plus. It is an easy slip to make when you copy deposit to write withdraw.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { balance = balance + amount; } void withdraw(int amount) { balance = balance + amount; } int getBalance() { return balance; }} class BankAccountTest { @Test void depositAddsToTheBalance() { BankAccount account = new BankAccount(100); account.deposit(50); assertEquals(150, account.getBalance()); } @Test void withdrawTakesFromTheBalance() { BankAccount account = new BankAccount(100); account.withdraw(30); assertEquals(70, account.getBalance()); }}The second line of the report now has a cross (✘) and a message: withdrawTakesFromTheBalance(): expected: <70> but was: <130>. Read it in three parts. First comes the name of the test, which tells you which behaviour broke. Then expected: <70>, the first argument you gave assertEquals. Then but was: <130>, what getBalance() really returned. 130 is 100 plus 30, which points straight at the plus sign in withdraw.
depositAddsToTheBalance still passes. Each test reports on its own, so one broken method does not hide the results of the others.
assertEqualstakes the right answer first and the code's answer second. Swap them and the test still passes and fails at the same moments, but its message points the wrong way: assertEquals(account.getBalance(), 70) reports expected: <130> but was: <70>, which reads as if the code had returned 70.expected: <70> but was: <130>, are JUnit's exact wording.new BankAccount(100), calls deposit(50), then runs assertEquals(150, account.getBalance()). It fails with expected: <150> but was: <100>. What does that tell you?The shape of a test#
Look at depositAddsToTheBalance again. It does three things in order, and almost every good test does the same three.
- 1Arrange what the test needs. Here, an account with 100 in it.
- 2Act by calling the one method the test is about. Here, deposit 50.
- 3Assert that the result is right. Here, the balance should now be 150.
The pattern is called arrange, act, assert. It gives every test the same layout, so a reader always knows where to look. Some people mark the three parts with comments, and others leave a blank line between them. The two tests below use one style each, and they bring in one more annotation.
import org.junit.jupiter.api.DisplayName;import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { balance = balance + amount; } int getBalance() { return balance; }} class BankAccountTest { @Test @DisplayName("A new account holds its opening balance") void newAccountHoldsItsOpeningBalance() { BankAccount account = new BankAccount(100); int balance = account.getBalance(); assertEquals(100, balance); } @Test @DisplayName("Depositing adds the amount to the balance") void depositAddsToTheBalance() { // Arrange BankAccount account = new BankAccount(100); // Act account.deposit(50); // Assert assertEquals(150, account.getBalance()); }}The report now shows sentences in place of method names. @DisplayName, imported from org.junit.jupiter.api like @Test, gives a test any name you like for the report, spaces included. JUnit still calls the method by its real name.
One behaviour per test
You could write one long test that deposits, withdraws and checks the balance after each step. It would work until something broke. Then its name could not tell you which step went wrong, and because a failed assertion ends the test on that line, the checks after it would never run. You would fix one bug, run again, and only then meet the next one. Two small tests give two separate answers at once.
One behaviour does not have to mean one assertEquals. A test that a refused deposit leaves the balance alone needs two checks, one for the refusal and one for the balance, and you will meet it in the next section. Both checks describe a single behaviour, so they belong in one test.
Names that say what should happen
A test's name is the first thing you read when it fails, so make it a sentence about the behaviour. withdrawTakesFromTheBalance tells you what broke, and test2 tells you nothing. Method names cannot contain spaces, so the words run together, each new word starting with a capital letter. When you want a real sentence in the report, add @DisplayName.
Key ideaEvery test has the same shape: arrange what it needs, act once, assert the result. Test one behaviour at a time, and name each test after what should happen.
StringBuilder's reverse passes, with arrange, act and assert in that order inside the test method.Tap a line, then tap where it should go. Or use the arrows.
The assertions you will use most#
assertEquals is one of many checks in JUnit's Assertions class. These are the ones you will reach for most often.
| Assertion | Passes when | Example |
|---|---|---|
assertEquals(expected, actual) | the two values are equal. Objects, strings included, are compared with equals, so two strings with the same text count as equal. | assertEquals("555-0102", number) |
assertEquals(expected, actual, delta) | two double values are no further apart than delta | assertEquals(0.3, total, 0.000001) |
assertTrue(condition) | the condition is true | assertTrue(book.has("Ada")) |
assertFalse(condition) | the condition is false | assertFalse(book.has("Bjarne")) |
assertNull(value) | the value is null | assertNull(book.numberFor("Bjarne")) |
assertNotNull(value) | the value is anything except null | assertNotNull(book.numberFor("Ada")) |
assertThrows(Type.class, () -> code) | running the code throws that type of exception | assertThrows(IllegalArgumentException.class, () -> account.deposit(-50)) |
True, false and null
A small phone book uses three of them. numberFor returns a person's number, or null when the name is not listed, and has answers yes or no.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PhoneBook { private String[] names = {"Ada", "Grace", "Linus"}; private String[] numbers = {"555-0101", "555-0102", "555-0103"}; String numberFor(String name) { for (int i = 0; i < names.length; i++) { if (names[i].equals(name)) { return numbers[i]; } } return null; } boolean has(String name) { return numberFor(name) != null; }} class PhoneBookTest { @Test void findsTheNumberForAListedName() { PhoneBook book = new PhoneBook(); assertEquals("555-0102", book.numberFor("Grace")); } @Test void saysYesForAListedName() { PhoneBook book = new PhoneBook(); assertTrue(book.has("Ada")); } @Test void saysNoForAnUnlistedName() { PhoneBook book = new PhoneBook(); assertFalse(book.has("Bjarne")); } @Test void givesNullForAnUnlistedName() { PhoneBook book = new PhoneBook(); assertNull(book.numberFor("Bjarne")); }}All four pass. assertEquals compared the two strings with equals, so it checked the text itself, which is what you want after the == trap from the references lesson. When one of these assertions fails, its message follows the pattern you already know. A failing assertTrue says expected: <true> but was: <false>, and a failing assertNull shows what it found instead, such as expected: <null> but was: <555-0102>.
Comparing doubles: the delta
Numbers with a decimal point need more care. Before you run this, decide what it prints.
public class Main { public static void main(String[] args) { double total = 0.1 + 0.2; System.out.println(total); System.out.println(total == 0.3); }}What do the two lines print?
Java prints 0.30000000000000004 and then false. A double stores numbers in binary, and most decimal fractions, 0.1 among them, have no exact binary form, just as 1/3 has no exact decimal form (0.333, with threes forever). Java stores the nearest value it can, and after some arithmetic the tiny gaps show.
So a test that compares doubles exactly can fail even when the code is right. This running log adds up distances in kilometres:
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class RunLog { private double totalKm; void addRun(double km) { totalKm = totalKm + km; } double getTotalKm() { return totalKm; }} class RunLogTest { @Test void addsTwoRunsExactly() { RunLog log = new RunLog(); log.addRun(0.1); log.addRun(0.2); assertEquals(0.3, log.getTotalKm()); } @Test void addsTwoRunsWithinADelta() { RunLog log = new RunLog(); log.addRun(0.1); log.addRun(0.2); assertEquals(0.3, log.getTotalKm(), 0.000001); }}The first test fails with expected: <0.3> but was: <0.30000000000000004>. The second passes, because its assertEquals has a third argument, the delta: the largest difference you are willing to accept. assertEquals(0.3, total, 0.000001) passes when the two numbers are within 0.000001 of each other. Pick a delta far smaller than any difference that matters in your program, and give one every time you compare doubles.
double altogether and use BigDecimal, which stores decimal fractions exactly. It has its own lesson: money and exact numbers.Testing that bad input is refused
In the encapsulation lesson, deposit refused a negative amount by printing a message and carrying on, and the code that called it never found out. The usual way for a Java method to refuse is to throw an exception: a signal that something went wrong, the same kind of signal that stopped your programs with an ArrayIndexOutOfBoundsException or a NullPointerException. The new deposit below starts with a check that ends in this line: throw new IllegalArgumentException("Deposit must be positive");
IllegalArgumentExceptionis the exception the JDK provides for “you passed a value I cannot accept”, and the text in quotes is its message. throw stops the method on the spot, so the balance never changes. If nothing deals with the exception, the whole program stops, as this one does. Step through it.
public class Main { public static void main(String[] args) { BankAccount account = new BankAccount(100); account.deposit(50); System.out.println("Balance: " + account.getBalance()); account.deposit(-50); System.out.println("Balance: " + account.getBalance()); }} class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit must be positive"); } balance = balance + amount; } int getBalance() { return balance; }}IllegalArgumentException: Deposit must be positive
A method was given a value it does not accept.
The first deposit works and the program prints Balance: 150. The second call throws, and the program stops with Exception in thread "main" java.lang.IllegalArgumentException: Deposit must be positive before it reaches the last line.
Refusing bad input is behaviour, so it deserves a test. assertThrows checks that a piece of code throws a given type of exception.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals;import static org.junit.jupiter.api.Assertions.assertThrows; class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit must be positive"); } balance = balance + amount; } int getBalance() { return balance; }} class BankAccountTest { @Test void depositRejectsANegativeAmount() { BankAccount account = new BankAccount(100); assertThrows(IllegalArgumentException.class, () -> account.deposit(-50)); } @Test void rejectedDepositLeavesTheBalanceAlone() { BankAccount account = new BankAccount(100); assertThrows(IllegalArgumentException.class, () -> account.deposit(-50)); assertEquals(100, account.getBalance()); }}Both tests pass. assertThrows takes two arguments. The first, IllegalArgumentException.class, names the type of exception you expect: .class after a class name refers to the class itself. The second, () -> account.deposit(-50), is the code to run.
That second argument is new syntax. Java normally runs a method call the moment it reaches it, and a call that throws would end the test before assertThrows could watch for anything. Writing () -> in front of the call packs the call up without running it and hands the package to assertThrows, which runs it inside its own safety net, catches what it throws and checks the type. For now, copy the shape: () -> followed by the call you want to test. The syntax is called a lambda, and it gets a full explanation in enums, lambdas and functional interfaces.
The second test checks two things about one behaviour, as promised: the deposit is refused, and the balance is still 100 afterwards.
assertThrows catch them. Catching them in your own code, with try and catch, is the subject of exception handling.assertThrows(IllegalArgumentException.class, () -> account.deposit(50));One more case. A test that calls account.deposit(-50) without assertThrows fails too. The exception escapes the test method, and the report gives it as the reason: java.lang.IllegalArgumentException: Deposit must be positive. Any exception you did not expect fails the test.
A fresh start for every test#
Every BankAccount test so far begins with the same line, BankAccount account = new BankAccount(100);. When several tests share a starting point, you can move it into a field and a method marked @BeforeEach. JUnit runs that method before each test.
import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class BankAccount { private int balance; BankAccount(int openingBalance) { balance = openingBalance; } void deposit(int amount) { balance = balance + amount; } void withdraw(int amount) { balance = balance - amount; } int getBalance() { return balance; }} class BankAccountTest { BankAccount account; @BeforeEach void setUp() { account = new BankAccount(100); } @Test void depositAddsToTheBalance() { account.deposit(50); assertEquals(150, account.getBalance()); } @Test void withdrawTakesFromTheBalance() { account.withdraw(30); assertEquals(70, account.getBalance()); }}Step through it and watch setUp run twice, once before each test, building a new account with 100 in it each time. The deposit test leaves 150 in its account, and the withdraw test never sees it: it starts from 100 and ends at 70.
Every test gets a new object
@BeforeEach is only half the story. In this test class, each test adds one to the field count, prints it and checks it. Predict the report.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class CounterTest { int count = 0; @Test void first() { count++; System.out.println("count is " + count); assertEquals(1, count); } @Test void second() { count++; System.out.println("count is " + count); assertEquals(1, count); }}Both tests add one to the same field. Which report does Run print?
Both tests print count is 1 and both pass. JUnit makes a new object of the test class for every test method, so each test starts with its fields freshly set: count is 0 again, and in BankAccountTest the field account holds a brand-new account. That is also why a field such as WordCounter counter = new WordCounter();, which you will meet in the next section, is safe in a test class. Every test gets its own.
This protects a rule that good tests follow: no test may depend on another. If the withdraw test only passed after the deposit test had run, its result would depend on the order, and JUnit does not promise to run tests in the order you wrote them. Its documentation says the order stays the same from run to run but is deliberately not obvious. A test that passes on its own and fails next to the others, or the other way round, can take hours to untangle.
Key ideaEach test builds its own starting point and leaves nothing behind for the next. JUnit makes a new test object for every test, and
@BeforeEachrebuilds the shared setup each time.
Edge cases: where bugs hide#
The tests so far use ordinary inputs, like 100 in the account and a sensible deposit. Code usually works for ordinary inputs, because those are the ones you pictured while writing it. Bugs gather where you did not look: at the ends of the allowed range, and at inputs so small or odd that they never came to mind. Inputs like that are called edge cases, and they tend to turn up in the same places:
- Empty input, such as the empty string
"", an empty array or an account holding 0. - Zero and negative numbers, anywhere a number comes in.
- Values right on a boundary. If 90 and above earns an A, test 90 and 89, the last value on each side.
- Exactly one of something, such as a one-letter word or an array with one element.
- Text with odd spacing, such as spaces at the start or the end, or two spaces in a row.
A test that finds a real bug
In string practice you planned a word counter: count the spaces and add one. For "hello there friend" that gives 3, which is right. Here is that plan as a class, with one ordinary test and three edge-case tests. Run it.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class WordCounter { int countWords(String text) { int spaces = 0; for (int i = 0; i < text.length(); i++) { if (text.charAt(i) == ' ') { spaces++; } } return spaces + 1; }} class WordCounterTest { WordCounter counter = new WordCounter(); @Test void countsTheWordsInASentence() { assertEquals(3, counter.countWords("hello there friend")); } @Test void findsNoWordsInEmptyText() { assertEquals(0, counter.countWords("")); } @Test void ignoresSpacesAtTheEnds() { assertEquals(2, counter.countWords(" hello world ")); } @Test void treatsTwoSpacesInARowAsOneGap() { assertEquals(2, counter.countWords("hello world")); }}Only the ordinary test passes. Each failure tells you something different.
findsNoWordsInEmptyText(): expected: <0> but was: <1>. Empty text has no spaces, so the method returns 0 plus 1.ignoresSpacesAtTheEnds(): expected: <2> but was: <4>. The spaces beforehelloand afterworldcount as gaps between words.treatsTwoSpacesInARowAsOneGap(): expected: <2> but was: <3>. Two spaces side by side count as two gaps.
“Spaces plus one” was only ever right for text with single spaces between words and none at the ends. Running it on a normal sentence would never show that, and three short tests showed it at once. Step through the empty-text test to see the cause: the loop never runs, spaces stays 0, and the method returns 1.
countWords so that all four tests pass. Change only the method, and leave the tests as they are.Key ideaBugs gather at the edges: empty input, zero, negative numbers and the values right at a limit. Test them on purpose, because ordinary inputs rarely find them.
withdraw method should refuse any amount larger than the balance. A teammate writes the check as if (amount >= balance). On an account holding 100, which test catches the mistake?One test, many inputs
Edge-case tests often look alike: the same call with a different input and a different expected answer. A parameterized test is written once and runs once per input. You mark the method @ParameterizedTest in place of @Test, give it a parameter, and add a source of values. @ValueSource lists values of one type. Here it feeds words to the palindrome checker from string practice.
import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.ValueSource; import static org.junit.jupiter.api.Assertions.assertFalse;import static org.junit.jupiter.api.Assertions.assertTrue; class Palindromes { boolean isPalindrome(String word) { for (int i = 0; i < word.length() / 2; i++) { if (word.charAt(i) != word.charAt(word.length() - 1 - i)) { return false; } } return true; }} class PalindromesTest { Palindromes checker = new Palindromes(); @ParameterizedTest @ValueSource(strings = {"racecar", "level", "noon", "a"}) void acceptsWordsThatReadTheSameBothWays(String word) { assertTrue(checker.isPalindrome(word)); } @ParameterizedTest @ValueSource(strings = {"java", "ab", "racecars"}) void rejectsOtherWords(String word) { assertFalse(checker.isPalindrome(word)); }}The report has one line per value, numbered from 1, with the value at the end, such as acceptsWordsThatReadTheSameBothWays(String) [1] racecar. Two short methods made seven checks. The word "a" is in the first list on purpose: a one-letter word is an edge case, and it does read the same both ways.
When each input has its own expected answer, use @CsvSource. Each string in it is one row, and the values in a row are separated by commas, like a line of a spreadsheet saved as CSV (comma-separated values). JUnit converts each value to the type of the matching parameter, so the row "90, A" becomes the int 90 and the String "A". This grader's table tests both sides of every boundary: 90 and 89, 80 and 79, 70 and 69.
import org.junit.jupiter.params.ParameterizedTest;import org.junit.jupiter.params.provider.CsvSource; import static org.junit.jupiter.api.Assertions.assertEquals; class Grader { String grade(int score) { if (score > 90) { return "A"; } else if (score >= 80) { return "B"; } else if (score >= 70) { return "C"; } else { return "F"; } }} class GraderTest { Grader grader = new Grader(); @ParameterizedTest @CsvSource({"100, A", "90, A", "89, B", "80, B", "79, C", "70, C", "69, F", "0, F"}) void givesTheRightLetter(int score, String expected) { assertEquals(expected, grader.grade(score)); }}One row fails: [2] 90, A: expected: <A> but was: <B>. The method says score > 90, so a score of exactly 90 falls through to B. The fix is score >= 90. A test with 95 would never have caught this. The row on the boundary did.
@ParameterizedTest, @ValueSource and @CsvSource live in a separate part of JUnit called junit-jupiter-params, which is why their imports start with org.junit.jupiter.params. A real project usually adds JUnit as one dependency, junit-jupiter, and that already includes it.Test first: red, green, refactor#
So far you wrote the code first and the tests afterwards. Test-driven development, or TDD, turns that around: you write a test for behaviour that does not exist yet, watch it fail, and only then write the code. Kent Beck, one of JUnit's two authors, made the practice popular. It runs in a short loop.
- 1Write one small test for the next piece of behaviour and run it. It fails, and most tools show a failure in red, so this is the red step. Seeing the test fail proves that it can catch the missing behaviour.
- 2Write the least code that makes every test pass, and nothing more. The tests go green.
- 3Tidy the code, with clearer names, less repetition or a simpler loop, and run the tests after each change to make sure they are still green.
Changing how code is written without changing what it does is called refactoring, and it names the third step. After it you go round the loop again with the next test. The example below builds a method that turns a full name into initials, so that Ada Lovelace becomes AL. Step through it, and read the report under the code at every step.
Write a test that fails
Start with what the method should do: Ada Lovelace gives AL. The class has a stub, a method that compiles but returns an empty string, so the test can run. It fails, which proves the test can catch the missing behaviour.
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class Names { String initials(String fullName) { return ""; }} class NamesTest { Names names = new Names(); @Test void twoNamesGiveTwoInitials() { assertEquals("AL", names.initials("Ada Lovelace")); }}What Run prints for this code
Three moves in that example are worth a second look.
- Step 1 starts from a stub, a method with a placeholder body that compiles but does nothing useful. In a real project the very first run often fails even earlier, with a compile error, because
initialsdoes not exist yet. Adding the stub is the quickest way from that error to a real red test. - Step 2 returns
"AL"for every name. That looks like cheating, and it is on purpose: it is the least code that passes, and it forces the next test to demand a real answer. Each step stays tiny, so when a test goes red you know which change caused it. - Step 7 rewrites the whole loop. Without tests that would be a gamble. With three tests that still pass after the change, you know the new code does everything the old code did.
Key ideaRed, green, refactor: write a failing test, make it pass with the simplest code, then tidy the code while the tests stay green. Then start again with the next test.
Writing the test first has a second benefit. It makes you decide what the method should do, its name, its inputs and its answer for each case, before you think about how. Some developers work this way for every change, and others save it for fiddly logic. Either way, the loop is worth practising until it feels normal.
3:07. The tests were written first. Write format so that all seven pass, including the one for a negative length.Running tests in a real project#
The snippets on this page put a class and its tests in one file, so that the Run button can show both. A real project keeps them apart. The class goes under src/main/java and its tests under src/test/java, in a test class named after it:
bank/├── pom.xml└── src/ ├── main/ │ └── java/ │ └── BankAccount.java └── test/ └── java/ └── BankAccountTest.javaThe test code can use every class in src/main/java, but it stays out of the finished program. Maven and Gradle, the two build tools most Java projects use, both expect this layout. The project also lists JUnit as a dependency, a library the build tool downloads for you. In Maven's build file, pom.xml, that is one entry:
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>6.1.3</version> <scope>test</scope> </dependency></dependencies><scope>test</scope>means JUnit is only used to compile and run the tests, and is never shipped with the program. Gradle declares the same library in its own format, together with JUnit's launcher, and adds useJUnitPlatform() to switch JUnit on. Setting up either tool from scratch is the job of Maven and Gradle. For now, read the output below as a preview.
In your editor
IntelliJ IDEA and VS Code show a small run button in the margin beside each test class and each test method. In Eclipse you right-click the class and choose Run As, then JUnit Test. The results appear in a panel that lists every test and marks which passed and which failed. Click a failed test to see the expected and actual values and to jump to the line that failed. Most editors can also create an empty test class for you, in the right folder.
From the terminal: mvn test
mvn testcompiles the project and runs every test class. Maven's test runner, a plug-in called Surefire, finds test classes by their names: by default, names that end in Test, Tests or TestCase, or start with Test. Here is the real output for the broken withdraw from earlier, with the compiling steps cut from the top:
$ mvn test...[INFO] -------------------------------------------------------[INFO] T E S T S[INFO] -------------------------------------------------------[INFO] Running BankAccountTest[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.034 s <<< FAILURE! -- in BankAccountTest[ERROR] BankAccountTest.withdrawTakesFromTheBalance -- Time elapsed: 0.008 s <<< FAILURE!org.opentest4j.AssertionFailedError: expected: <70> but was: <130> at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:569) at BankAccountTest.withdrawTakesFromTheBalance(BankAccountTest.java:18) [INFO][INFO] Results:[INFO][ERROR] Failures:[ERROR] BankAccountTest.withdrawTakesFromTheBalance:18 expected: <70> but was: <130>[INFO][ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0[INFO][INFO] ------------------------------------------------------------------------[INFO] BUILD FAILURE[INFO] ------------------------------------------------------------------------Read it from the bottom. Tests run: 2, Failures: 1 is the count, and the list under Failures: names the test, the line in BankAccountTest.javawhere it failed (18), and JUnit's message. Maven also counts Errors. A test whose assertion failed is a failure, and a test stopped by an exception nobody expected is an error. Once withdraw is fixed, the same command ends with Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 and BUILD SUCCESS.
From the terminal: gradle test
Gradle says less. When every test passes, it prints only that the build succeeded. A failure looks like this, and a project that comes with the Gradle wrapper runs ./gradlew test and gets the same output:
$ gradle test > Task :test FAILED BankAccountTest > withdrawTakesFromTheBalance() FAILED org.opentest4j.AssertionFailedError at BankAccountTest.java:18 2 tests completed, 1 failed FAILURE: Build failed with an exception. * What went wrong:Execution failed for task ':test'.> There were failing tests. See the report at: file:///home/you/bank/build/reports/tests/test/index.html * Try:> Run with --scan to get full insights from a Build Scan (powered by Develocity). BUILD FAILED in 3s3 actionable tasks: 1 executed, 2 up-to-dateGradle names the failing test and its line, and saves the full details, JUnit's message included, in the HTML report it points to. Open that file in a browser to read them.
JUnit's own report
Editors and build tools all hand the work to the same engine, the JUnit Platform: the part of JUnit that finds tests and runs them. JUnit also ships a console launcher that runs the platform from the terminal, and its output shows JUnit's own report style. For the same two tests:
$ javac -d out -cp junit-platform-console-standalone-6.1.3.jar BankAccount.java BankAccountTest.java$ java -jar junit-platform-console-standalone-6.1.3.jar execute --class-path out --scan-class-path Thanks for using JUnit! Support its development at https://junit.org/sponsoring ╷├─ JUnit Platform Suite ✔├─ JUnit Jupiter ✔│ └─ BankAccountTest ✔│ ├─ depositAddsToTheBalance() ✔│ └─ withdrawTakesFromTheBalance() ✘ expected: <70> but was: <130>└─ JUnit Vintage ✔ Failures (1): JUnit Jupiter:BankAccountTest:withdrawTakesFromTheBalance() MethodSource [className = 'BankAccountTest', methodName = 'withdrawTakesFromTheBalance', methodParameterTypes = ''] => org.opentest4j.AssertionFailedError: expected: <70> but was: <130> org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:569) BankAccountTest.withdrawTakesFromTheBalance(BankAccountTest.java:18) Test run finished after 73 ms[ 4 containers found ][ 0 containers skipped ][ 4 containers started ][ 0 containers aborted ][ 4 containers successful ][ 0 containers failed ][ 2 tests found ][ 0 tests skipped ][ 2 tests started ][ 0 tests aborted ][ 1 tests successful ][ 1 tests failed ]JUnit draws a tree. At the top sit its engines: Jupiter runs the tests on this page, and the launcher also brings Vintage, for old JUnit 4 tests, and Platform Suite, for grouping test classes. Under Jupiter come the test class and each test with a tick or a cross, then the failures in detail, then a table of counts. The visualizer's report is a shorter version of the same information: one line per test with JUnit's own messages, then a count. Whichever tool you use, you are looking for the same three things: which tests failed, what each one expected, and what the code gave instead.
Try it
Answer each one in your head or on paper first, then open the answer.
Playlist list = new Playlist();, then list.add("Blue Monday");, then assertEquals(1, list.size());. Which line arranges, which acts, and which asserts?assertEquals(total, 30); and fails with expected: <25> but was: <30>. Apart from the failure itself, what is wrong with the test?assertEquals(0.3, log.getTotalKm()) fail when the code is right, and what do you write instead?int largest(int[] numbers) that returns the biggest number in an array.account in a field, BankAccount account = new BankAccount(100);, and both tests deposit 50 and expect 150. Do both pass?assertThrows(IllegalArgumentException.class, () -> account.deposit(-5)), what does the () -> do?Practice
Work through these on your own. Each one builds on this page.
Counter from the methods lesson, with its increment and add methods. Cover an ordinary case for each method, then decide what add(0) and add(-3) should do and write a test for each decision.BankAccount a withdraw that refuses an amount of zero or less, and an amount larger than the balance, each with an IllegalArgumentException. Build it test first, one rule at a time, with red, green, refactor. Include the boundary: withdrawing the whole balance must work and leave 0.@CsvSource to test the vowel counter from string practice with at least six rows. Include a word with no vowels, such as rhythm, a word in capital letters, and the empty string, which a CSV row writes as two single quotes: "'', 0".String reverseWords(String sentence) test first, so that "hello world" gives "world hello". Start with one word, then two, then three, then the edge cases: empty text and extra spaces. Write down the red message you see at each step.BankAccountTest with its @BeforeEach, and put one bug at a time into BankAccount: a plus in place of a minus, a constructor that ignores the opening balance, a deposit that adds 1 whatever the amount. Run the tests after each bug. If any bug survives with every test green, write the test that catches it.Further reading#
- The JUnit User Guide, covering test classes, every annotation, the test instance lifecycle and parameterized tests
- The Assertions class in the JUnit API documentation, with every assertion and the order of its arguments
- Using the JUnit Platform with Maven's Surefire plug-in
- Testing in Java and JVM projects, from the Gradle user manual
Spotted a mistake, or a part that did not make sense? Report it by email