Java: testing a method with a tiny test runner

Check normal cases, boundaries, and error cases, and report which ones pass.

Java
class Main {
  static int clamp(int value, int low, int high) {
    if (low > high) {
      throw new IllegalArgumentException("low > high");
    }
    return Math.max(low, Math.min(high, value));
  }

  static int passed = 0;
  static int failed = 0;

  static void check(String name, int expected, int actual) {
    if (expected == actual) {
      passed++;
      System.out.println("PASS " + name);
    } else {
      failed++;
      System.out.println("FAIL " + name + ": expected " + expected + " but was " + actual);
    }
  }

  public static void main(String[] args) {
    check("inside range", 5, clamp(5, 0, 10));
    check("below range", 0, clamp(-3, 0, 10));
    check("above range", 10, clamp(99, 0, 10));
    try {
      clamp(1, 5, 2);
      failed++;
      System.out.println("FAIL bad bounds: no exception");
    } catch (IllegalArgumentException e) {
      passed++;
      System.out.println("PASS bad bounds throws: " + e.getMessage());
    }
    System.out.println(passed + " passed, " + failed + " failed");
  }
}
Output
PASS inside range
PASS below range
PASS above range
PASS bad bounds throws: low > high
4 passed, 0 failed

Explanation

A unit test calls one piece of code with known inputs and compares the result with the expected value. Good tests cover a typical value, the boundaries, and the error cases. Real Java projects use a framework such as JUnit, but the idea is the same: arrange, act, assert.

This program tests clamp, which limits a value to a range. It checks a value inside the range, below it, above it, and a bad call that must throw. Each check prints PASS or FAIL with the expected and actual values, so a failure explains itself.

Explore the dry run
Guided walkthroughReady
1class Main {
2 static int clamp(int value, int low, int high) {
3 if (low > high) {
4 throw new IllegalArgumentException("low > high");
5 }
6 return Math.max(low, Math.min(high, value));
7 }
8
9 static int passed = 0;
10 static int failed = 0;
11
12 static void check(String name, int expected, int actual) {
13 if (expected == actual) {
14 passed++;
15 System.out.println("PASS " + name);
16 } else {
17 failed++;
18 System.out.println("FAIL " + name + ": expected " + expected + " but was " + actual);
19 }
20 }
21
22 public static void main(String[] args) {
23 check("inside range", 5, clamp(5, 0, 10));
24 check("below range", 0, clamp(-3, 0, 10));
25 check("above range", 10, clamp(99, 0, 10));
26 try {
27 clamp(1, 5, 2);
28 failed++;
29 System.out.println("FAIL bad bounds: no exception");
30 } catch (IllegalArgumentException e) {
31 passed++;
32 System.out.println("PASS bad bounds throws: " + e.getMessage());
33 }
34 System.out.println(passed + " passed, " + failed + " failed");
35 }
36}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
A typical value is returned unchanged.expected 5, actual 5
PASS inside range
A value below the minimum is raised to it.expected 0, actual 0
PASS inside range
PASS below range
A value above the maximum is lowered to it.expected 10, actual 10
PASS inside range
PASS below range
PASS above range
A final summary counts the results, including the expected exception.passed = 4, failed = 0
PASS inside range
PASS below range
PASS above range
PASS bad bounds throws: low > high
4 passed, 0 failed

Cost and common mistakes

Each check is O(1); the runner is linear in the number of tests.

  • A test that never fails proves little. Make sure each test would fail if the code were wrong.
  • Tests should be independent of each other and of the order they run in.
  • Catching the exception in the test is part of the check: if no exception is thrown, the test must fail.

Try it yourself

Add a boundary test for clamp(10, 0, 10).

Reveal answer and reasoning

check("at upper bound", 10, clamp(10, 0, 10)); the value is already equal to the maximum, so it is returned unchanged.

Further reading