Java • Debugging and Testing

Writing Test Cases — Write simple test cases and expected outputs for methods

Learn to verify your Java methods work correctly by writing test cases that compare actual outputs against expected results.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Why Testing Exists: A Brief History

Imagine you build a calculator app and hand it to a friend. They type 2 + 2 and get 5. Embarrassing, right? That scenario is exactly the kind of mistake that software testing was invented to catch before anyone sees it. Since the earliest days of computing, programmers have needed systematic ways to verify that their code does what it is supposed to do. Writing test cases — small checks that compare a method's actual output to its expected output — is the most fundamental form of that verification.

1947 • The First 'Bug'

Grace Hopper's team found a literal moth stuck in the Harvard Mark II relay, coining the term debugging. Early testing meant manually checking each output by hand.

1979 • Structured Testing Emerges

Glenford Myers published The Art of Software Testing, formalizing the idea that every program should be verified against clearly stated expected results.

1997 • JUnit Is Born

Kent Beck and Erich Gamma created JUnit, a Java framework that lets you write and run test cases automatically. It became the industry standard for Java testing.

2003+ • Test-Driven Development

The practice of writing test cases before writing the actual code gained popularity. Today, testing is considered essential at every level of professional software development.

The core question that testing answers has never changed: Does this code actually produce the output I expect? In this lesson, you will learn how to answer that question by writing simple, effective test cases for Java methods.

SECTION 2

Core Principles of Writing Test Cases

A test case is a small piece of code that calls a method with specific inputs and then checks whether the result matches what you expect. Think of it as a question you ask your program: 'If I give you 3 and 4, do you give me back 7?' Good test cases follow a handful of important principles.

1

Known Input → Known Output

Every test case starts with a specific input and a specific expected output that you calculate by hand or determine from the specification. If the method's actual result differs, the test fails.
2

Test Normal, Edge, and Error Cases

Don't just test the 'happy path.' Include edge cases (like zero, empty strings, or very large numbers) and inputs that should cause errors or special behavior.
3

One Behavior Per Test

Each test case should check exactly one thing. If a test fails, you want to know immediately what went wrong without untangling multiple checks.
4

Independent and Repeatable

A good test does not depend on any other test running first. You should be able to run it a hundred times and get the same result each time.
5

Descriptive Names

Name your test methods so that anyone reading them understands what is being checked. For example, testAddPositiveNumbers is far better than test1.
✦

KEY TAKEAWAY

Writing a test case is like checking your math homework with an answer key. You already know the correct answer; the test simply confirms whether your code agrees. If your code says 2 + 2 = 5, the test catches that mismatch instantly so you can fix it before anyone else notices.

SECTION 3

Anatomy of a Test Case

Every test case follows a three-part pattern often called Arrange–Act–Assert. First you set up the inputs (Arrange), then you call the method (Act), and finally you compare the actual output to the expected output (Assert). The diagram below shows how these three phases connect.

Arrange – Act – Assert PatternARRANGESet up inputsint a = 3;int b = 4;int expected = 7;ACTCall the methodint actual = Calculator.add(a, b);ASSERTCompare resultsactual == expected7 == 7 → PASS ✓Full Test Case Codepublic static void testAddPositiveNumbers() {// Arrangeint expected = 7;// Actint actual = Calculator.add(3, 4);// Assertif (actual == expected)System.out.println("PASS");elseSystem.out.println("FAIL");
The Arrange–Act–Assert pattern: set up inputs (cyan), call the method (violet), and compare the result to the expected output (pink). The code box at the bottom shows how these phases look in actual Java.

In the diagram above, the Arrange phase prepares values like a = 3, b = 4, and expected = 7. The Act phase calls Calculator.add(a, b) and stores the result. The Assert phase checks if the actual output equals 7. If it does, the test passes; if not, it fails and tells you something is wrong.

SECTION 4

How Test Cases Work in Java

In a high school Java course, you typically don't need a full testing framework like JUnit. Instead, you can write test cases directly in your main method using simple if statements. The core mechanism is straightforward: call the method, store the result, and compare it to the expected value using == for primitives or .equals() for objects and Strings.

Comparison Operators for Assertions

Data TypeComparison ApproachExample
int, double, booleanUse ==actual == 7
StringUse .equals()actual.equals("hello")
ArraysUse Arrays.equals()Arrays.equals(actual, expected)
double (precision)Use tolerance: Math.abs(a − b) < 0.001Math.abs(actual − 3.14) < 0.001

Choosing the right comparison for different data types

A Reusable Test Helper Method

To avoid repeating the same if/else block for every test, you can write a small helper method. This helper takes a test name, the expected value, and the actual value, then prints PASS or FAIL. Here is a pattern you can reuse across all your assignments:

💡

Helper Method Pattern

Create a method called check(String testName, int expected, int actual) that prints the test name and whether the expected value matches the actual value. You can overload this method for different return types like String or boolean.

SECTION 5

Types of Test Cases You Should Write

Choosing what to test is just as important as knowing how to test. A single normal-case test is a good start, but it will miss many bugs. Strong test suites cover several categories of inputs. The diagram below illustrates the four main categories you should consider for every method you test.

Four Categories of Test Cases1Normal CasesTypical, everyday inputs.add(3, 4) → 7max(10, 20) → 20greet("Alice") → "Hello, Alice"These confirm basic correctness.2Edge CasesBoundary or extreme inputs.add(0, 0) → 0max(-1, -5) → -1greet("") → "Hello, "These catch off-by-one & boundary bugs.3Special / Error CasesInvalid or unexpected inputs.divide(10, 0) → error/messageindexOf(null) → -1sqrt(-4) → NaN or errorThese verify graceful error handling.4Equivalence PartitionsOne test per "group" of similar inputs.isPositive(5) → true (pos group)isPositive(-3) → false (neg group)isPositive(0) → false (zero group)These ensure all input groups are covered.
Four categories of test cases: Normal cases verify typical behavior, edge cases target boundaries, special/error cases handle invalid input, and equivalence partitions cover distinct input groups.

A strong set of test cases includes at least one example from each of these four categories. For a method like findMax(int[] arr), you might test a normal array like {3, 7, 1} (normal case), an array of one element (edge case), an empty array (error case), and arrays with all negative numbers versus all positive numbers (equivalence partitions). This combination gives you high confidence that the method works correctly.

SECTION 6

Worked Example: Testing a GradeCalculator Method

Suppose you have a method called getLetterGrade(int score) that returns a String: "A" for scores 90–100, "B" for 80–89, "C" for 70–79, "D" for 60–69, and "F" for anything below 60. Let's write a complete set of test cases for this method.

Writing Test Cases for getLetterGrade(int score)

  1. 1

    Step 1 — Identify the Method Signature

    The method takes an int parameter called score and returns a String. Since the return type is String, we will use .equals() for our assertions, not ==.
    Signature: public static String getLetterGrade(int score)
  2. 2

    Step 2 — Write Normal Case Tests

    Pick one representative value from each grade range. For example: score 95 should return "A", score 85 should return "B", score 75 should return "C", score 65 should return "D", and score 50 should return "F". These five tests cover every normal output.
    check("Normal A", "A", getLetterGrade(95)); … and four similar calls for B, C, D, F.
  3. 3

    Step 3 — Write Edge Case Tests (Boundaries)

    The boundaries between grades are where bugs hide. Test the exact cutoff values: 90 (lowest A), 89 (highest B), 80 (lowest B), 79 (highest C), 70, 69, 60, 59, 100 (perfect score), and 0 (lowest possible). Each boundary checks that the if/else conditions use >= and < correctly.
    check("Boundary A", "A", getLetterGrade(90)); and check("Boundary B high", "B", getLetterGrade(89));
  4. 4

    Step 4 — Write Special / Error Case Tests

    What should happen if someone passes in a negative score or a score above 100? The specification doesn't say, but you should still test these to make sure the method doesn't crash. For instance, getLetterGrade(-5) should probably return "F", and getLetterGrade(105) might return "A" or an error message — document what you expect.
    check("Negative", "F", getLetterGrade(-5));
  5. 5

    Step 5 — Assemble and Run All Tests

    Place all test calls inside your main method. Run the program. Every test should print PASS. If any test prints FAIL, the output tells you exactly which case broke, making it easy to find and fix the bug. A well-named test like "Boundary B high" immediately tells you the problem is around score 89.
    Expected console output: all lines show PASS. Any FAIL line pinpoints the bug.
💡

Pro Tip

Always write your test cases before you start fixing a bug. That way, you can run the tests again after your fix and confirm the bug is actually gone. If you skip this step, you might accidentally introduce a new bug while fixing the old one.

SECTION 7

Strengths and Limitations of Simple Test Cases

Writing test cases in your main method is a great starting point, but like any tool, it has both strengths and limitations. Understanding these will help you decide when to use this approach and when to step up to more advanced testing tools.

StrengthsLimitations
No extra libraries or setup needed — works in any Java environment.No automatic test discovery — you have to call each test method manually.
Easy to read and understand, even for beginners.If one test crashes (e.g., NullPointerException), remaining tests don't run.
Teaches the core logic of testing: input → expected → compare.No built-in pass/fail counting or summary report at the end.
Fast feedback — run the program and see results immediately.Comparing complex objects (like ArrayLists) requires extra helper code.
Prepares you for professional frameworks like JUnit.Tests live in the same file as production code, which can get messy.

Comparing strengths and limitations of simple main-method testing

✦

KEY TAKEAWAY

Simple test cases in main are like training wheels on a bicycle: they teach you balance and confidence, and the skills transfer directly when you upgrade to a professional testing framework like JUnit later. The habit of thinking 'What should this method return for this input?' is the most valuable skill, and it works at every level of programming.

SECTION 8

From Simple Tests to Professional Frameworks

The simple testing approach you've learned in this lesson maps directly onto professional testing frameworks. Once you are comfortable writing test cases by hand, the transition to tools like JUnit is surprisingly smooth because the underlying logic is identical — only the syntax changes.

ConceptSimple (main method)JUnit 5 (Professional)
Marking a method as a testYou call it manually in mainAdd @Test annotation above the method
Checking equalityif (actual == expected)assertEquals(expected, actual)
Reporting resultsSystem.out.println("PASS/FAIL")Framework shows green/red indicators automatically
Handling crashesProgram stops; later tests don't runEach test runs independently; one failure doesn't stop others
Setup / teardownManually reset variables@BeforeEach and @AfterEach annotations

How simple testing maps to JUnit 5

Notice that every row in the table shows the same logical idea — the difference is just convenience and automation. When you move to AP Computer Science or college-level courses, you'll appreciate that the mental model you built here carries over completely. The Arrange–Act–Assert pattern, choosing edge cases, and naming tests clearly are skills that never change, regardless of the tool you use.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain in your own words what the three phases of the Arrange–Act–Assert pattern do. Why is it important to determine the expected output before running the test?
Show answer
In the Arrange phase, you set up the inputs and any objects the method needs. In the Act phase, you call the method with those inputs and store the result. In the Assert phase, you compare the actual result to the expected result. Determining the expected output first is crucial because it forces you to understand what the method should do, based on the specification, not what it actually does. If you look at the output first and then write the test to match, you're just confirming the current behavior — which might be buggy.

Question 2: BASIC CALCULATION

A method public static int multiply(int a, int b) returns the product of two integers. Write three test cases (as Java code using System.out.println) that test: (a) two positive numbers, (b) one number being zero, and (c) two negative numbers. State the expected output for each.
Show answer
Test (a): int actual = multiply(6, 7); // expected: 42 — Two positive numbers should give a positive product. Test (b): int actual = multiply(5, 0); // expected: 0 — Anything multiplied by zero is zero; this is an important edge case. Test (c): int actual = multiply(-3, -4); // expected: 12 — Two negatives multiply to a positive. Each test would compare actual == expected and print PASS or FAIL accordingly.

Question 3: INTERMEDIATE

A method public static String reverseString(String s) returns the reverse of the input string. List five test cases covering normal, edge, and special cases. For each, state the input and the expected output.
Show answer
(1) Normal: reverseString("hello") → "olleh". (2) Edge — single character: reverseString("a") → "a" because reversing one character changes nothing. (3) Edge — empty string: reverseString("") → "" because there is nothing to reverse. (4) Normal — palindrome: reverseString("racecar") → "racecar", confirming palindromes stay the same. (5) Special — spaces and punctuation: reverseString("hi there!") → "!ereht ih". These five tests cover everyday inputs, boundaries, and special characters. Remember to compare with .equals() because the return type is String.

Question 4: APPLIED

You are building a simple banking app. The method public static double withdraw(double balance, double amount) should return the new balance after a withdrawal. If the amount exceeds the balance, it should return the original balance unchanged (no overdraft allowed). Write a complete Java main method with at least four test cases, including at least one edge case and one error case. Use a helper method for assertions.
Show answer
Your main method should include: (1) Normal: withdraw(100.0, 30.0) → 70.0 (basic subtraction). (2) Edge — withdraw entire balance: withdraw(50.0, 50.0) → 0.0 (exact boundary). (3) Error — overdraft attempt: withdraw(20.0, 25.0) → 20.0 (balance unchanged because amount > balance). (4) Edge — zero withdrawal: withdraw(100.0, 0.0) → 100.0. Because the return type is double, use a tolerance-based assertion like Math.abs(actual − expected) < 0.001 to avoid floating-point comparison issues. A helper method checkDouble(String name, double expected, double actual) keeps the code clean and avoids repeating the tolerance logic.

Question 5: CRITICAL THINKING

A classmate writes a method public static boolean isPrime(int n) and claims it works because they tested isPrime(7) and got true. Explain why one test case is not sufficient. Design a set of at least six test cases that would give you strong confidence in the method's correctness. For each, explain which category (normal, edge, error) it falls into and why you chose it.
Show answer
One test is insufficient because it only verifies a single prime number — it does not check whether the method correctly returns false for non-primes, handles edge cases, or rejects invalid input. A strong test set: (1) Normal prime: isPrime(13) → true. (2) Normal non-prime: isPrime(15) → false (15 = 3 × 5). (3) Edge — smallest prime: isPrime(2) → true (2 is the only even prime, a common source of bugs). (4) Edge — the number 1: isPrime(1) → false (1 is not prime by definition). (5) Edge — even number: isPrime(4) → false (tests that even-number logic works). (6) Error/special — negative number: isPrime(-7) → false (primes are defined for positive integers greater than 1). Together these six tests cover true-positive, true-negative, boundaries, the special case of 2, and invalid input. This combination is far more convincing than a single test.

SUMMARY

Lesson Summary

Summary

A test case is a small piece of code that verifies whether a method produces the correct output for a given input. Every test case follows the Arrange–Act–Assert pattern: set up inputs, call the method, and compare the actual result to the expected output. Use == for primitives and .equals() for Strings and objects. A strong test suite covers four categories: normal cases, edge cases (boundaries and extremes), error cases (invalid inputs), and equivalence partitions (one test per distinct input group).

Give every test a descriptive name, keep each test focused on one behavior, and always determine the expected output before running the code. These habits prepare you for professional tools like JUnit and the practice of test-driven development. Writing test cases is not just a school exercise — it's one of the most important skills in real-world software development.

Varsity Tutors • Java • Writing Test Cases — Write simple test cases and expected outputs for methods