Unit Testing Java Code with JUnit

Explains how to use JUnit to do unit testing in Java.

Prerequisites

Objectives

Definitions

Unit
A part of a program, ranging from as small as a single method to as large as all the classes in a package, often a single class
Unit test
Code to test a unit of a program
JUnit
An open source library of Java code that aids unit testing of Java programs; available from junit.org. JUnit’s purpose is to automate the verfication of unit test results.

How to Use JUnit

In order to use JUnit to test one or more Java methods, do the following.

  1. Write one or more Java methods in a class.
  2. In a separate Java file import your Java class and the JUnit classes.
  3. Within the same file as the previous step, write a class that contains test methods. Each test method must be annotated with @org.junit.Test.
  4. Within each test method, write code to call your Java methods, then write one or more calls to org.junit.Assert.assertEquals or the other assert methods.
  5. Run the test class that you wrote.
  6. When you run your test class, assertEquals will compare the value returned from your Java method to the value that you expect your Java method will return.

The assertEquals method accepts two or three parameters, namely:

assertEquals(expected, actual, delta);
Compares the expected value to the actual value.
expected: the value that you expect your Java function to return.
actual: the value that your Java function returned.
delta: only needed for floats or doubles; the maximum difference between expected and actual that you're willing to accept and still consider the numbers to be equal.

There are several other assert methods in the org.junit.Assert class, including assertFalse, assertTrue, assertNull, assertSame, and assertArrayEquals. See the online java docs for junit.

Example

The following four Java classes contain simple methods that simulate a bank that offers savings accounts and checking accounts to its customers. If you’re learning JUnit for the first time, the details of the four Java classes may not be important right now. You may want to skip down to examine the TestBank class which uses JUnit.

import java.util.HashMap;

public class Bank {
    // Use the singleton design pattern to ensure that
    // the computer creates only one Bank object each
    // time it runs this program.
    private static final Bank singleton = new Bank();

    public static Bank getInstance() {
        return singleton;
    }

    public static int getNextAccountId() {
        int id;
        synchronized(singleton) {
            id = singleton.nextAccountId;
            singleton.nextAccountId = id + 1;
        }
        return id;
    }


    private final HashMap<Integer, Account> accounts;
    private int nextAccountId;

    private Bank() {
        this.accounts = new HashMap<Integer, Account>();
        this.nextAccountId = 1;
    }

    public void addAccount(Account account) {
        this.accounts.put(account.getAccountId(), account);
    }

    public Account getAccount(int accountId) {
        return this.accounts.get(accountId);
    }
}
import java.util.Date;

public abstract class Account {
    private final int accountId;
    private final Date dateOpened;
    protected double balance;

    public Account() {
        this.accountId = Bank.getNextAccountId();
        this.dateOpened = new Date();
        this.balance = 0;
    }

    public int getAccountId() {
        return this.accountId;
    }

    public double getBalance() {
        return this.balance;
    }

    public Date getDateOpened() {
        return this.dateOpened;
    }

    public abstract void deposit(double amount);
    public abstract void withdraw(double amount);

    @Override
    public int hashCode() {
        return this.accountId;
    }
}
public class SavingsAccount extends Account {
    public static final double MINIMUM_BALANCE = 25.0;


    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Incorrect amount; deposit amount is " +
                amount + " but must be greater than 0.");
        }
        this.balance += amount;
    }


    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Incorrect amount; withdrawal amount is " +
                amount + " but must be greater than 0.");
        }
        double newBalance = this.balance - amount;
        if (newBalance < MINIMUM_BALANCE) {
            throw new IllegalArgumentException(
                "Insufficient funds to withdraw " + amount +
                ". Current balance is " + this.balance +
                ". Balance after withdrawal would be " +
                + newBalance + " which is less than the" +
                " minimum balance of " + MINIMUM_BALANCE);
        }
        this.balance = newBalance;
    }
}
public class CheckingAccount extends Account {
    public static final double OVERDRAFT_LIMIT = 500;
    public static final double OVERDRAFT_FEE = 25;


    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Incorrect amount; deposit amount is " +
                amount + " but must be greater than 0.");
        }
        this.balance += amount;
    }


    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Incorrect amount; withdrawal amount is " +
                amount + " but must be greater than 0.");
        }
        double newBalance = this.balance - amount;
        if (newBalance < 0) {
            throw new IllegalArgumentException(
                "Insufficient funds to withdraw " + amount +
                ". Current balance is " + this.balance +
                ". Balance after withdrawal would be " +
                newBalance + ".");
        }
        this.balance = newBalance;
    }


    public void settleCheck(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Incorrect amount; check amount is " +
                amount + " but must be greater than 0.");
        }
        double newBalance = this.balance - amount;
        if (newBalance < 0) {
            newBalance -= OVERDRAFT_FEE;
            if (newBalance < -OVERDRAFT_LIMIT) {
                throw new IllegalArgumentException(
                    "Insufficient funds to pay a check for " +
                    amount + ". Current balance is " +
                    this.balance + ". Balance after paying" +
                    " the check and overdraft fee would be " +
                    newBalance + " which is less than the" +
                    " overdraft limit.");
            }
        }
        this.balance = newBalance;
    }
}

The TestBank class below uses JUnit to test the previous four Java classes. Notice that the test methods call the assertEquals method and other assert methods many times. Each time it is called, the assertEquals method will compare an expected value to an actual value. This is how test methods automatically verify that a method works correctly.

import org.junit.Test;
import static org.junit.Assert.*;
import java.util.Date;


public class TestBank {
    private static final double DELTA = 0.001;


    // The @Test annotation must appear
    // before each unit test method.
    @Test
    public void testBank() {
        Bank bank = Bank.getInstance();

        // Add two accounts to the bank.
        Account sa = new SavingsAccount();
        Account ca = new CheckingAccount();
        bank.addAccount(sa);
        bank.addAccount(ca);

        // Find the first the account that was just added.
        Account f = bank.getAccount(sa.getAccountId());

        // Verify that the getAccount method returned the
        // correct account.
        assertSame(f, sa);

        // Find the second the account that was just added.
        f = bank.getAccount(ca.getAccountId());
        assertSame(f, ca);

        // Attempt to find an account that doesn't exist.
        f = bank.getAccount(-1);
        assertNull(f);
    }


    @Test
    public void testSavingsAccount() {
        SavingsAccount acc = new SavingsAccount();

        // Verify that SavingsAccount extends Account.
        assertTrue(acc instanceof SavingsAccount);
        assertTrue(acc instanceof Account);

        // Verify that the account's date opened is correct.
        Date now = new Date();
        Date opened = acc.getDateOpened();
        assertEquals(now.getTime(), opened.getTime(), 1000);

        // Verify that the new account's balance is 0.
        assertEquals(0, acc.getBalance(), 0);

        // Deposit $50 and ensure the balance is $50.
        acc.deposit(50);
        assertEquals(50, acc.getBalance(), DELTA);

        // Withdraw $25 and ensure the balance is $25.
        acc.withdraw(25);
        assertEquals(25, acc.getBalance(), DELTA);

        // Attempt to withdraw more money than is in the
        // account and ensure the withdraw method throws
        // an exception and doesn't change the balance.
        IllegalArgumentException thrown = null;
        try {
            acc.withdraw(40);
        }
        catch (IllegalArgumentException ex) {
            thrown = ex;
        }
        assertNotNull(thrown);
        assertEquals(25, acc.getBalance(), DELTA);
    }
}