Home › Guides › Java
Java9 min readCode verified

Testing, assert and Wrapper Classes: Proving a Method Works

A method that compiles is not a method that works. This guide is about the gap between the two: how to choose test cases that catch mistakes, what assert really does, how to hunt down a wrong answer, and the wrapper classes that turn "42" into 42. Every listing here was compiled and run on OpenJDK 21.

Taste as you go#

A good cook does not make an entire meal and only then find out the sauce was salty. They taste the sauce on its own, then the rice on its own, and fix each part while it is still small and easy to fix.

Testing a program works the same way. A unit test checks one small piece of code, usually one method, on its own. If it fails, you know exactly which piece to look at, instead of staring at a whole program that prints the wrong thing.

This page covers the four ideas Java courses group together here: choosing test cases, the assert statement, finding a logic error, and the wrapper classes, which are the classes that hold a single primitive value such as an int inside an object.

What a test case is#

A test case is a pair: an input, and the output you expect for it. Both halves matter. An input with no expected answer is not a test, because there is nothing to compare against.

Here is a method that is supposed to charge $2 per day once a library book is more than 3 days late, and four test cases for it:

public class FeeTest {
    // Should charge $2 per day, but only once a book is more than 3 days late.
    public static int lateFee(int daysLate) {
        if (daysLate >= 3) {
            return daysLate * 2;
        }
        return 0;
    }

    public static void check(int input, int expected) {
        int actual = lateFee(input);
        String result = (actual == expected) ? "PASS" : "FAIL";
        System.out.println(result + "  lateFee(" + input + ") = " + actual + ", expected " + expected);
    }

    public static void main(String[] args) {
        check(0, 0);
        check(10, 20);
        check(3, 0);
        check(4, 8);
    }
}
PASS  lateFee(0) = 0, expected 0
PASS  lateFee(10) = 20, expected 20
FAIL  lateFee(3) = 6, expected 0
PASS  lateFee(4) = 8, expected 8

Three tests pass and one fails, and the failing one points straight at the bug. The rule said more than 3 days, which is daysLate > 3. The code says >= 3, so a book exactly 3 days late is charged when it should not be.

Notice which tests found it. check(0, 0) and check(10, 20) pass whether the code says > or >=. They are far from the line, so they cannot tell the two apart.

Test the edges, not just the middle#

Mistakes crowd around the edges of a rule: > typed as >=, a loop that runs one time too many, a range that forgets its last value. So a good set of tests always includes the boundary values, which are the inputs sitting right on either side of the line.

Kind of testFor lateFeeWhy
typical10the ordinary case works at all
boundary3 and 4the last free day and the first charged day
smallest0nothing late, nothing owed
invalid-2what should happen is a decision; the test forces you to make it

For a method with several paths, give every path at least one test. A method returning the larger of two numbers needs three: first larger, second larger, and the tie that everyone forgets.

Unit tests and regression tests#

Two phrases turn up on exams, and they answer different questions.

TermQuestion it answers
unit testingDoes this one method, on its own, give the right answer for each chosen input?
regression testingAfter I changed something, does everything that used to work still work?

A regression is a step backwards: a bug that comes back, or a feature that quietly stops working because of an unrelated change. You catch it by keeping your old tests and re-running all of them after every change. That is why the check calls above are kept in the file rather than deleted once they pass.

assert, and why it does nothing by default#

assert is a one-line promise: this must be true here, and if it is not, stop the program. The form is the keyword, a condition, a colon, and a message.

public class Checked {
    public static void main(String[] args) {
        int stock = 5;
        stock = stock - 8;
        assert stock >= 0 : "stock went negative: " + stock;
        System.out.println("finished, stock = " + stock);
    }
}
finished, stock = -3

The stock went to -3 and the program carried on regardless. That is not a bug in the listing. Assertions are switched off unless you ask for them, and a plain java Checked does not ask. Run it with the -ea flag, short for “enable assertions”, and the promise is checked:

java -ea Checked
Exception in thread "main" java.lang.AssertionError: stock went negative: -3
	at Checked.main(Checked.java:5)

Now the promise is checked, it is false, and the program stops right there. The first line is your message. The second says where: line 5 of Checked.java. The final println never runs.

When not to use assert

Never use assert to check something a user typed. Assertions can be switched off, so the check might simply not happen. assert is for your own mistakes: things that should be impossible if your code is right. Anything that can go wrong in normal use needs an if or a loop that always runs.

Finding a logic error#

There are three kinds of error, and they show up at different times.

KindWhen you find outExample
compile errorjavac refuses to build ita missing semicolon, a wrong type
run-time errorit builds, then stops with an exception"cat".charAt(3)
logic errorit runs to the end and prints the wrong answerthe one below

The logic error is the hardest, because nothing announces it. The way in is to print the values at each step and look for the first one that is wrong:

public class Average {
    public static void main(String[] args) {
        int first = 2;
        int second = 3;
        double average = (first + second) / 2;
        System.out.println("step 1, sum:     " + (first + second));
        System.out.println("step 2, average: " + average);
    }
}
step 1, sum:     5
step 2, average: 2.0

The sum is right. The average is wrong. So the mistake is in the line between them: (first + second) / 2 is an int divided by an int, which throws the fraction away and gives 2, and only then is it stored in a double as 2.0. Divide by 2.0 instead:

public class Average2 {
    public static void main(String[] args) {
        int first = 2;
        int second = 3;
        double average = (first + second) / 2.0;
        System.out.println("average: " + average);
    }
}
average: 2.5

A debugger does the same job without the extra println lines. You set a breakpoint, which is a line where the program will pause, and then look at every variable and step forward one line at a time.

Wrapper classes: a box around one value#

Primitives such as int and double are plain values. They are fast, but they are not objects, so they have no methods and cannot be null. Each one has a partner class that puts the value in a box, and that box is an object.

PrimitiveWrapper class
intInteger
charCharacter
doubleDouble
booleanBoolean
long, short, byte, floatLong, Short, Byte, Float

Most are the primitive's name with a capital letter. The two to learn separately are Integer and Character.

The wrappers also carry useful static methods, and these are what you will use most:

public class Parse {
    public static void main(String[] args) {
        String typed = "42";
        System.out.println(typed + 8);
        System.out.println(Integer.parseInt(typed) + 8);
        System.out.println(Double.parseDouble("2.5") * 2);
        System.out.println(Character.isDigit('7') + " " + Character.toUpperCase('q'));
    }
}
428
50
5.0
true Q

The first line is the trap. "42" + 8 joins text and gives 428. Integer.parseInt turns the text into a real number first, so the second line adds and gives 50. This is exactly what you need when a number arrives as text, from a file, a command line or a text box.

parseInt does not round or guess. Integer.parseInt("4.5") and Integer.parseInt("forty") both stop the program with a NumberFormatException, because neither is a whole number written in digits.

Autoboxing, unboxing and null#

Java moves values in and out of the box for you. Putting a primitive into its wrapper is called autoboxing; taking it back out is unboxing.

public class Boxes {
    public static void main(String[] args) {
        Integer boxed = 7;
        int plain = boxed + 1;
        Integer nothing = null;
        System.out.println(boxed + " " + plain + " " + nothing);
        System.out.println(Integer.MAX_VALUE);
        System.out.println(Integer.MAX_VALUE + 1);
    }
}
7 8 null
2147483647
-2147483648

Integer boxed = 7; boxes the 7 automatically. boxed + 1 unboxes it so it can be added. And an Integer can be null, meaning “no value at all”, which an int can never be.

The last two lines are worth remembering. Integer.MAX_VALUE is the biggest number an int can hold. Add one and it silently overflows: it wraps round to the most negative int, like a car odometer rolling past its last digit. There is no error and no warning.

One sentence to carry into the exam

A test is an input plus the answer you expect, and the best inputs sit right on the edge of the rule. assert checks your own promises only when run with -ea; wrapper classes box a primitive so it can be an object, and parseInt turns text into a number.

Where these go wrong#

  • Testing only easy, middle-of-the-range values. They pass whether the code says > or >=. Test right on the boundary.
  • A test with no expected answer. Printing a result and glancing at it is not a test. Write down what it should be first.
  • Thinking a silent assert means the condition held. Without -ea it was never checked.
  • Using assert to validate input. It can be turned off. Use if or a validation loop.
  • Dividing two ints and storing the answer in a double. The fraction is already gone. Make one side a double first.
  • Writing Int or Char. The wrappers are Integer and Character.
  • Adding a number to text you meant to parse. "42" + 8 is 428. Parse first.

Test yourself in the free Kestrel Exams app

Topic-selectable practice — offline, no ads, no account.

Practice testing and wrappers →

Frequently asked questions#

What is the goal of unit testing?

To check that one small piece of code, usually one method, gives the right result for chosen inputs when it is run on its own. Because each piece is tested separately, a failing test tells you exactly where the mistake is, rather than leaving you to search a whole program.

What is the difference between unit testing and regression testing?

Unit testing checks that a single method works. Regression testing re-runs tests that used to pass after you change something, to make sure the change did not break anything that already worked. The same tests are often used for both; the difference is why and when you run them.

What should a good set of test cases include?

A typical value, the boundary values on each side of every rule, the smallest or empty case, and at least one invalid input. For a rule like 'more than 3 days late', test 3 and 4, because a > written as >= only shows up right on the line.

Why does my Java assert statement not do anything?

Because assertions are disabled by default. A plain java MyProgram skips every assert statement. Run it with java -ea MyProgram to enable them. When an enabled assert is false, Java throws an AssertionError with your message and the program stops.

What is a wrapper class in Java?

A class whose object holds one primitive value: Integer for int, Double for double, Character for char, Boolean for boolean, and so on. Wrappers let a primitive be used where an object is needed, allow the value null, and provide static helpers such as Integer.parseInt and Character.isDigit.

What does Integer.parseInt do?

It turns text containing a whole number, such as "42", into the int 42. It does not round or guess: "4.5" or "forty" makes it throw a NumberFormatException. Double.parseDouble does the same job for decimal numbers.

Suggest a change

Something here not clear? A topic you wish we covered? Tell us. We read every message, and a request is the fastest way to get a guide written — several of these exist because somebody asked.