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.

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.

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.

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.

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.

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

Choosing the right comparison for different data types
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

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.

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 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.

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
Step 1 — Identify the Method SignatureThe 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
Step 2 — Write Normal Case TestsPick 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
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
Step 4 — Write Special / Error Case TestsWhat 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
Step 5 — Assemble and Run All TestsPlace 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.

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.

Comparing strengths and limitations of simple main-method testing
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.
✦ 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.

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.

How simple testing maps to JUnit 5
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

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.

Practice Problems

PROBLEM 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?
PROBLEM 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.
PROBLEM 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.
PROBLEM 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.
PROBLEM 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.

Lesson 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