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.
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.
Known Input → Known Output
Test Normal, Edge, and Error Cases
One Behavior Per Test
Independent and Repeatable
Descriptive Names
testAddPositiveNumbers is far better than test1.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.
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
| Data Type | Comparison Approach | Example |
|---|---|---|
int, double, boolean | Use == | actual == 7 |
String | Use .equals() | actual.equals("hello") |
| Arrays | Use Arrays.equals() | Arrays.equals(actual, expected) |
double (precision) | Use tolerance: Math.abs(a − b) < 0.001 | Math.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:
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.
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.
int parameter called score and returns a String. Since the return type is String, we will use .equals() for our assertions, not ==.public static String getLetterGrade(int score)check("Normal A", "A", getLetterGrade(95)); … and four similar calls for B, C, D, F.if/else conditions use >= and < correctly.check("Boundary A", "A", getLetterGrade(90)); and check("Boundary B high", "B", getLetterGrade(89));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));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.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.
| Strengths | Limitations |
|---|---|
| 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. |
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.
| Concept | Simple (main method) | JUnit 5 (Professional) |
|---|---|---|
| Marking a method as a test | You call it manually in main | Add @Test annotation above the method |
| Checking equality | if (actual == expected) | assertEquals(expected, actual) |
| Reporting results | System.out.println("PASS/FAIL") | Framework shows green/red indicators automatically |
| Handling crashes | Program stops; later tests don't run | Each test runs independently; one failure doesn't stop others |
| Setup / teardown | Manually 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
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.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.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.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.