Single-Class Unit Test with Data-Driven Test Helper

Single-Class Unit Test with Data-Driven Test Helper - Guide

Single-Class Unit Test with Data-Driven Test Helper

This pattern lets you write a self-testing Java class with no external test framework. One class contains four functions. Each function has one job. Test inputs and expected results are listed in one place and driven through a single reusable helper.

The name IcyHot comes from the worked example at the bottom of this page. The pattern applies to any target function - temperatures are just the teaching example.

Starting Point - The Problem and the Target Function

Every instance of this pattern begins with two things: a problem statement and a target function signature. Nothing else is written until these are clear.

Problem statement:

Given two temperatures, return true if one is less than 0 AND the other is greater than 100. Either temperature can be the icy one or the hot one.

Examples:

icyHot(120, -1)  → true
icyHot(-1, 120)  → true
icyHot(2,  120)  → false

Target function signature:

public boolean icyHot(int temp1, int temp2)

This is the function you have been asked to implement. You do not write its body yet. The signature is enough to build everything else - the test helper mirrors it, and the test cases call it by name.

1. The Four Functions

The class file lists the functions in this order from top to bottom:

# Function Label Job
1 main ENTRY Creates one instance of the class. Calls testCases. Contains no other logic.
2 testCases TEST LIST Lists all test inputs and expected results. Calls testHelper once per scenario. Contains no assertions.
3 testHelper ASSERTION Calls the target function. Compares actual result to expected result. Increments testCaseCount and testCaseErrors. This is the data-driven heart of the pattern - one helper handles every test row.
4 target (e.g. icyHot) TARGET The function the question asks you to write. The only function that contains business logic.

The class also has two fields: int testCaseCount (total tests run) and int testCaseErrors (total failures).

2. Order of Writing - Given a New Target

The functions are not written in file order. The target is stubbed first so the test helper has a signature to call. The implementation is filled in last so the tests are ready before the logic is written.

Step 1 - Write the target stub. Define the function signature. Return a placeholder value (return false; or return 0;). Do not write the logic yet.
Step 2 - Write the test helper. Mirror the target function signature. Add one extra parameter at the end for the expected return value. Call the target. Compare actual to expected. Track errors.
Step 3 - Write the test cases. Call the test helper once per scenario. Cover the happy path, boundaries, and reversed inputs. Write the expected value by hand before implementing the logic - this forces you to think through the problem first.
Step 4 - Write main. Create an instance. Call testCases. Nothing else.
Step 5 - Implement the target function. Fill in the logic. Run the class. The tests report how many passed and how many failed. The aim is 0 errors.
File order vs. writing order: The file reads: main → testCases → testHelper → target.
You write: target (stub) → testHelper → testCases → main → target (logic).
The file is organized by abstraction level (high to low). You write by dependency (what must exist before what can call it).

3. Second Target Example - maxDouble

Requirements

  1. Given two integers a and b.
  2. Find the larger of the two integers.
  3. Return that larger integer multiplied by two.

Step 1 - Target Function Stub

Write the signature first. Return 0 as a placeholder. Do not write the logic yet.

public int maxDouble(int a, int b) {
    return 0; // stub - implement in Step 5
}

Step 2 - Test Helper

The test helper follows the same template as testIcyHot. Only 4 things change: the method name, the input parameters, the return type, and the local variable default. The error-counting and diagnostic structure stays identical.

ParttestIcyHot (original)testMaxDouble (new)
Method nametestIcyHottestMaxDouble
Input paramsint temp1, int temp2int a, int b
Expected paramboolean expectedReturnint expectedReturn
Local variableboolean actualReturn = false;int actualReturn = 0;
Target callicyHot(temp1, temp2)maxDouble(a, b)
ComparisonactualReturn != expectedReturnactualReturn != expectedReturn
private void testMaxDouble(int a, int b, int expectedReturn) {
    int actualReturn = 0;
    testCaseCount++;
    try {
        actualReturn = maxDouble(a, b);
    } catch (Throwable e) {
        e.printStackTrace();
        testCaseErrors++;
        System.err.println("Error " + e + ", expected:" + expectedReturn
                + ", for a:" + a + ", b:" + b
                + ", count:" + testCaseCount + ".");
        return;
    }
    if (actualReturn != expectedReturn) {
        System.out.println("Actual:" + actualReturn + ", expected:" + expectedReturn
                + ", for a:" + a + ", b:" + b
                + ", count:" + testCaseCount + ".");
        testCaseErrors++;
    }
}

The rest of the class - testCases for maxDouble, the implementation body, and main - follows the same structure shown in the full IcyHot example below. The test helper is the only function that changes with each new target.

4. IcyHot.java - Full Example

The complete source file. The implementation is intentionally incomplete - it only handles the case where temp1 is icy and temp2 is hot. The reversed case surfaces as a deliberate test failure to demonstrate that good test cases expose incomplete logic.




Version 5