SECTION 1
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.
1947 • The First 'Bug'
1979 • Structured Testing Emerges
1999 • Unit Testing Goes Mainstream
2014–Now • JavaScript Testing Matures
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.
SECTION 2
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
KEY TAKEAWAY
SECTION 3
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.
SECTION 4
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:
MANUAL TEST PATTERN
You run the code and visually check if the output is 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.
ASSERT PATTERN
The first argument is a boolean condition. If it evaluates to 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.
CUSTOM TEST HELPER
The 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.
Why === instead of ==?
=== 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.SECTION 5
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.
Boundary Testing Tip
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.SECTION 6
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.
Writing Test Cases for getLetterGrade(score)
- 1
Step 1 — Start with Normal Cases
Pick a typical value from each grade range. These are your "happy path" tests that verify the basic logic works correctly.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'); - 2
Step 2 — Add Boundary Cases
Test the exact boundary values where the grade changes. These catch off-by-one errors — for example, does a score of 90 get an "A" or a "B"? Test on both sides of each cutoff.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'); - 3
Step 3 — Add Edge Cases for Extreme Values
What happens at the absolute extremes of the valid range? And what about values that shouldn't even be possible, like negative scores or scores above 100?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'); - 4
Step 4 — Review and Count Your Tests
You now have 15 test cases covering normal behavior, boundaries, and edge cases. Run all of them. If any fail, you've found a bug — or you've found that your expected output needs adjusting based on how the function is supposed to handle invalid input.Total: 5 normal + 6 boundary + 4 edge = 15 test cases
KEY TAKEAWAY
SECTION 7
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. |
Comparison of four testing approaches, from simplest to most powerful
CHOOSING YOUR APPROACH
SECTION 8
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 |
How current skills map to professional testing practices
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
Question 2: BASIC CALCULATION
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.Show answer
console.assert(isEven(4) === true, '4 should be even');
console.assert(isEven(7) === false, '7 should be odd');
console.assert(isEven(10) === true, '10 should be even');
Each test case picks a typical input, calls the function, and compares the result to the expected boolean value using strict equality (===). The string argument provides a descriptive message that will appear in the console if the assertion fails.Question 3: INTERMEDIATE
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.Show answer
test('0 is even', isEven(0), true);
test('-2 is even', isEven(-2), true);
test('-3 is odd', isEven(-3), false);
test('2.5 is not an integer', isEven(2.5), false);
Zero is a classic edge case because students often forget whether it's even or odd (it's even). Negative numbers test whether the function handles the minus sign correctly. The decimal value 2.5 tests whether the function only considers integers. These edge cases probe the boundaries of the function's expected behavior and are the inputs most likely to reveal hidden bugs.Question 4: APPLIED
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.Show answer
test('standard tip', calculateTip(50, 20), 10); — Normal case with typical values.
test('15% on $100', calculateTip(100, 15), 15); — Another normal case with a round bill.
test('0% tip', calculateTip(50, 0), 0); — Boundary: what if the customer tips nothing?
test('100% tip', calculateTip(50, 100), 50); — Boundary: extreme generosity.
test('bill is $0', calculateTip(0, 20), 0); — Edge: zero bill amount should produce zero tip.
test('decimal bill', calculateTip(33.33, 18), 5.9994); — Edge: non-round numbers produce decimals.
This suite covers everyday usage, boundary values where the percentage is at its extremes (0% and 100%), and edge cases like a $0 bill and decimal arithmetic. The decimal test also helps you think about whether the function should round its output.Question 5: CRITICAL THINKING
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.Show answer
findMax([-3, -1, -7]) should return -1, but it returns 0. The bug is that max starts at 0, which is larger than all elements, so no element ever replaces it. The fix is to initialize max to arr[0] or -Infinity.
Bug 2 — Empty array: findMax([]) should return undefined or throw an error, but it returns 0. The loop never executes, so the initial value of max (0) is returned, which is misleading because 0 isn't in the array.
Bug 3 — Single element: findMax([5]) should return 5 and actually does (since 5 > 0), but findMax([-5]) should return -5 but returns 0 — same bug as with all negatives.
This exercise demonstrates why a single test case is never enough to declare that code 'works.' The classmate tested only one normal case with positive numbers and missed the entire class of negative-number inputs. Systematic edge-case testing would have caught these bugs immediately.SUMMARY
Lesson Summary
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.