public class ContohUjiException {

    // ---- exception domain ----

    static final class SaldoKurangException extends RuntimeException {

        SaldoKurangException(String pesan) {
            super(pesan);
        }
    }

    static final class SetoranTidakSahException extends RuntimeException {

        SetoranTidakSahException(String pesan, Throwable sebab) {
            super(pesan, sebab);
        }
    }

    // ---- kode produksi yang diuji jalur gagalnya ----

    static final class Rekening {

        private long saldo;

        Rekening(long saldoAwal) {
            this.saldo = saldoAwal;
        }

        void tarik(long jumlah) {
            if (jumlah <= 0) {
                throw new IllegalArgumentException("jumlah tarik harus positif: " + jumlah);
            }
            if (jumlah > saldo) {
                throw new SaldoKurangException(
                    "saldo " + saldo + " tidak cukup untuk " + jumlah);
            }
            saldo -= jumlah;
        }

        void setor(String jumlahTeks) {
            long jumlah;
            try {
                jumlah = Long.parseLong(jumlahTeks);
            } catch (NumberFormatException sebab) {
                throw new SetoranTidakSahException(
                    "setoran bukan angka: '" + jumlahTeks + "'", sebab);
            }
            saldo += jumlah;
        }

        long saldo() {
            return saldo;
        }
    }

    // ---- assertThrows mini ala JUnit ----

    static <T extends Throwable> T harusMelempar(Class<T> jenis, Runnable blok) {
        try {
            blok.run();
        } catch (Throwable terlempar) {
            if (jenis.isInstance(terlempar)) {
                return jenis.cast(terlempar);
            }
            throw new AssertionError("jenis salah: "
                + terlempar.getClass().getSimpleName()
                + ", diharapkan " + jenis.getSimpleName());
        }
        throw new AssertionError("tidak ada exception terlempar");
    }

    public static void main(String[] args) {
        Rekening rekening = new Rekening(100_000L);

        // test 1: jenis dan pesan exception saldo kurang
        SaldoKurangException saldoKurang = harusMelempar(SaldoKurangException.class,
            () -> rekening.tarik(150_000L));
        System.out.println("jenis  : " + saldoKurang.getClass().getSimpleName());
        System.out.println("pesan  : " + saldoKurang.getMessage());

        // test 2: aturan masukan pada jumlah tarik
        IllegalArgumentException masukanSalah = harusMelempar(IllegalArgumentException.class,
            () -> rekening.tarik(-500L));
        System.out.println("pesan  : " + masukanSalah.getMessage());

        // test 3: setoran tidak sah membawa cause dari parsing
        SetoranTidakSahException setoran = harusMelempar(SetoranTidakSahException.class,
            () -> rekening.setor("dua ribu"));
        System.out.println("pesan  : " + setoran.getMessage());
        System.out.println("cause  : " + setoran.getCause().getClass().getSimpleName()
            + " -> " + setoran.getCause().getMessage());

        // seluruh operasi gagal: saldo tidak berubah sama sekali
        System.out.println("saldo  : " + rekening.saldo());
    }
}
