Why Testing Exists: A Brief History
Imagine you build a calculator app and hand it to a friend. They type in 2 + 2 and get 5. Embarrassing, right? Now imagine that calculator is controlling a spacecraft or a hospital machine. Throughout the history of computing, software bugs have caused rocket failures, financial losses, and even endangered lives. The practice of software testing grew out of a simple need: we have to prove our code actually does what we think it does before we ship it into the world.
So here's the central question: how do you actually write a test case? What exactly goes into one, and how do you make sure you haven't missed a scenario that could break your code? That's what this lesson is all about.
Core Principles of Writing Test Cases
A test case is a simple statement that says: "If I give this function these inputs, I expect to get this output." If the actual output matches your expected output, the test passes. If it doesn't, the test fails, and you know something is wrong with your function.
Input → Output Thinking
Normal Cases First
Edge Cases Matter
One Test, One Behavior
Descriptive Names
Anatomy of a Test Case
The diagram below shows the three essential parts of every test case. You start with an input, send it through the function under test, and compare the actual output to your expected output. If they match, the test passes. If they differ, the test fails.
Notice that the expected output is something you decide beforehand, based on what the function is supposed to do. You're not asking the computer what the answer is — you're telling it what the answer should be, and then checking if the code agrees. This is what makes testing so powerful: it catches mismatches between your intentions and your code's behavior.
How Test Cases Work in JavaScript
You don't need a fancy testing framework to start writing test cases. In JavaScript, the simplest approach is a basic console.log comparison. As you grow as a programmer, you'll graduate to tools like console.assert() and eventually professional frameworks. Let's look at three levels of writing test cases.
Level 1: Manual Comparison with console.log
The most basic test case simply calls your function and prints the result. You eyeball it to see if it matches what you expected. Here's how you'd test a function called double that should multiply a number by 2:
10. This works but is slow and error-prone — you might miss a wrong answer.Level 2: Automated Checking with console.assert()
JavaScript has a built-in console.assert() method that does the comparison for you. It stays silent when the test passes, but prints an error message when it fails. This is much better because failures announce themselves — you don't have to scan through output line by line.
true, nothing happens. If false, the error message appears in the console.Level 3: A Simple Test Function
You can write your own tiny test helper that prints clear pass/fail messages. This pattern is actually how professional testing frameworks work under the hood — just with a lot more features added on top.
name parameter describes what the test checks. The actual parameter is the function's return value, and expected is the correct answer you decided in advance.=== checks both value and type (strict equality), while == can convert types and produce surprising results. For example, 0 == "" is true but 0 === "" is false. Always use === in your test comparisons to avoid false passes.Identifying and Categorizing Edge Cases
Normal test cases check that your function works with typical inputs. But the real skill in testing is thinking about edge cases — the unusual, extreme, or unexpected inputs that often reveal hidden bugs. Different types of functions tend to have different kinds of edge cases. The diagram below maps out the most common categories.
A great way to think about edge cases is to imagine you're trying to break your own function. What's the weirdest, most unlikely input someone could pass in? A professional developer once said, "Write code as if the person using it is actively trying to destroy it." That mindset will help you write better edge-case tests.
if (x > 10), always test three values: one below the boundary (9), one at the boundary (10), and one above (11). Off-by-one errors are among the most common bugs in programming.Worked Example: Testing a gradeCalculator Function
Let's write a complete set of test cases for a function called getLetterGrade(score) that takes a numeric score (0–100) and returns a letter grade: "A" for 90–100, "B" for 80–89, "C" for 70–79, "D" for 60–69, and "F" for anything below 60.
test('95 → A', getLetterGrade(95), 'A');
test('85 → B', getLetterGrade(85), 'B');
test('75 → C', getLetterGrade(75), 'C');
test('65 → D', getLetterGrade(65), 'D');
test('50 → F', getLetterGrade(50), 'F');test('90 → A (boundary)', getLetterGrade(90), 'A');
test('89 → B (boundary)', getLetterGrade(89), 'B');
test('80 → B (boundary)', getLetterGrade(80), 'B');
test('79 → C (boundary)', getLetterGrade(79), 'C');
test('60 → D (boundary)', getLetterGrade(60), 'D');
test('59 → F (boundary)', getLetterGrade(59), 'F');test('0 → F (minimum)', getLetterGrade(0), 'F');
test('100 → A (maximum)', getLetterGrade(100), 'A');
test('-5 → invalid?', getLetterGrade(-5), 'Invalid');
test('105 → invalid?', getLetterGrade(105), 'Invalid');Comparing Testing Approaches
As you gain more experience, you'll encounter different ways to organize and run your test cases. Each approach has strengths and trade-offs. The table below compares the three approaches we've discussed, plus a preview of professional testing frameworks you'll encounter later.
| Approach | Strengths | Limitations |
|---|---|---|
| console.log() | Easy to understand. No setup needed. Good for quick checks while learning. | Requires manual checking — you have to read every line. Easy to miss failures in long output. |
| console.assert() | Built into JavaScript. Only shows output when a test fails, so passing tests stay quiet. | Limited error messages. Doesn't count how many tests passed/failed. No grouping. |
| Custom test() function | Clear pass/fail labels. Descriptive test names. Shows expected vs. actual on failure. | You have to write and maintain the helper function yourself. Limited to simple equality checks. |
| Testing framework (Jest, Vitest) | Powerful matchers, auto-discovery of test files, code coverage reports, and watch mode. | Requires installation and configuration. Steeper learning curve. Overkill for small scripts. |
From Simple Tests to Professional Testing
The simple test cases you're writing now are the foundation of a much larger testing discipline. In the professional world, developers write hundreds or even thousands of automated tests that run every time someone changes the code. Understanding how your current skills connect to that bigger picture will motivate you to develop strong testing habits now.
| What You're Learning Now | Professional Version |
|---|---|
| Writing test cases with inputs and expected outputs | Unit testing — testing individual functions in isolation using frameworks like Jest |
| Checking edge cases manually | Property-based testing — tools automatically generate hundreds of edge cases to test your function |
| Running tests by pressing 'Run' | Continuous Integration (CI) — tests run automatically every time code is pushed to a shared repository |
| Comparing actual === expected | Assertion libraries — provide rich matchers like toBe(), toEqual(), toThrow(), and toContain() |
| Deciding what to test by intuition | Code coverage tools — measure which lines of code your tests exercise, highlighting blind spots |
One particularly important concept you'll encounter is Test-Driven Development (TDD), where you write the test cases before you write the function. It sounds backwards, but it forces you to think carefully about what the function should do before you worry about how to code it. The test-case-writing skills you're building right now are the essential first step toward TDD.
Practice Problems
isEven(num) returns true if the number is even and false if it is odd. Write three normal test cases using the console.assert() pattern.isEven(num) function, write four edge-case test cases. Think about zero, negative numbers, and non-integer values. Use the custom test() helper function pattern from this lesson.calculateTip(billAmount, tipPercent) that returns the tip amount as a number. For example, calculateTip(50, 20) should return 10. Write a complete test suite of at least 6 test cases that includes normal cases, boundary cases, and edge cases. Explain why you chose each one.findMax([3, 7, 2]) and getting 7:
function findMax(arr) { let max = 0; for (let i = 0; i < arr.length; i++) { if (arr[i] > max) max = arr[i]; } return max; }
Identify at least three test cases that would expose bugs in this function. For each test case, state the input, the expected output, what the function actually returns, and explain why the bug occurs.Lesson Summary
A test case is a structured check that pairs a specific input with an expected output, runs the function, and compares the actual output to see if they match. You should always start with normal cases (typical inputs), then add boundary cases (values right at the cutoff points), and finally include edge cases (zeros, empty values, negatives, and extremes).
In JavaScript, you can write test cases using console.assert() or a custom test() helper function that prints clear pass/fail messages. Each test should check one behavior and have a descriptive name. Always use strict equality (===) for comparisons. The testing skills you build now — choosing inputs, predicting outputs, and hunting for edge cases — are the foundation of professional unit testing and test-driven development.