SECTION 1
Historical Context & Motivation
Imagine you build a calculator app for a class project. It works perfectly on Monday, but after you add a new feature on Tuesday, the addition function suddenly returns wrong answers. You didn't mean to break anything — you just didn't have a way to automatically check that your old code still worked. This is exactly the problem that test cases solve. A test case is a small piece of code that runs your function with known inputs and checks whether the output matches what you expect.
The idea of automated testing has evolved over decades as software grew more complex. Early programmers tested their code by manually running it and checking the output with their own eyes. As programs grew from hundreds to millions of lines, manual testing became impossible. Developers needed machines to do the checking for them, which led to the concept of assertions — statements that declare "this condition must be true, or something is wrong."
1960s • Manual Debugging Era
1979 • assert() in C Language
assert() macro, letting developers embed automatic checks directly inside their code. This idea influenced every language that followed.1999 • JUnit & Unit Testing Movement
2010s • JavaScript Testing Frameworks
Today • Testing as a Requirement
The core question that testing answers is deceptively simple: "Does my function actually do what I think it does?" Without test cases, you're relying on hope. With them, you have proof.
SECTION 2
Core Principles & Definitions
Before writing test cases, you need to understand a few foundational ideas. A test case is a small, self-contained piece of code that calls a function with specific inputs and checks whether the output matches the expected result. An assertion is the actual check — a statement that says "I assert this condition is true." If the condition is true, the test passes silently. If it's false, the test fails loudly, alerting you to the problem.
Test Case
Assertion
console.assert(2 + 2 === 4) passes because the math is correct.Expected vs. Actual
Edge Case
Test Suite
KEY TAKEAWAY
SECTION 3
Visual Explanation — The Anatomy of a Test Case
The diagram above illustrates the Arrange-Act-Assert pattern (sometimes called AAA), which is the most common way to structure a test case in any programming language. In the Arrange step, you set up the inputs and any data your function needs. In the Act step, you call the function and capture its return value. In the Assert step, you use an assertion to compare the actual return value against the expected value. If they match, the test passes quietly. If they don't, an error message tells you exactly what went wrong.
SECTION 4
How Assertions Work in JavaScript
JavaScript gives you several ways to write assertions. The simplest is the built-in console.assert() method. It takes a condition as its first argument and an optional error message as its second. If the condition evaluates to true, nothing happens — silence means success. If the condition evaluates to false, the message is printed to the console as an error.
console.assert() Syntax
ASSERTION SYNTAX
condition — any expression that evaluates to true or false. errorMessage — a string describing what went wrong, displayed only when the condition is false.
You can also write your own simple assertion function. This gives you more control over the output and helps you understand what's happening under the hood. Here's the basic logic:
CUSTOM ASSERTION LOGIC
If the condition is falsy (false, 0, null, undefined, etc.), the function throws an error with your custom message. If the condition is truthy, execution continues normally — the test passes.
A critical detail is the use of strict equality (===) rather than loose equality (==) in your assertions. Strict equality checks both the value and the type, which prevents subtle bugs. For example, 5 == '5' is true in JavaScript (loose equality coerces types), but 5 === '5' is false (strict equality does not). Always use === in your test assertions to catch type-related bugs.
Why Not Just Use console.log()?
console.log() and reading them yourself, but this doesn't scale. If you have 50 functions, you'd need to manually read 50 outputs every time you change your code. Assertions automate this: they stay silent when things work and scream when they don't. You only need to pay attention when something breaks.SECTION 5
Types of Test Cases You Should Write
A single test case only tells you about one specific input. To gain confidence that a function truly works, you need to write multiple test cases covering different scenarios. Think of it like studying for a test — reviewing just one practice problem isn't enough. You need to practice with normal cases, edge cases, and error cases.
isEven() function.Normal cases test the everyday, expected behavior. These are the inputs your function was primarily designed to handle. Edge cases test the boundaries — zero, negative numbers, extremely large values, or the first and last items in a list. Error cases test what happens when someone passes the wrong type of data or missing values. A function that handles all three categories gracefully is a robust function.
- ✓ Normal cases — Test with common, expected inputs that represent typical usage.
- ✓ Edge cases — Test with boundary values like 0, empty strings, and negative numbers.
- ✓ Error cases — Test with invalid inputs like wrong data types, null, or undefined.
SECTION 6
Worked Example — Testing a gradeCalculator Function
Let's walk through a complete example. Suppose you write a function called getLetterGrade(score) that takes a numerical 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. We'll write a full set of test cases using assertions.
Writing Test Cases for getLetterGrade(score)
- 1
Step 1 — Define the Function
First, here's the function we want to test:function getLetterGrade(score) { if (score >= 90) return 'A'; if (score >= 80) return 'B'; if (score >= 70) return 'C'; if (score >= 60) return 'D'; return 'F'; }Our job is to write assertions that verify this function works correctly for a variety of inputs. - 2
Step 2 — Write Normal Case Tests
We test typical scores from each grade range:console.assert(getLetterGrade(95) === 'A', 'Score 95 should be A');console.assert(getLetterGrade(85) === 'B', 'Score 85 should be B');console.assert(getLetterGrade(75) === 'C', 'Score 75 should be C');console.assert(getLetterGrade(65) === 'D', 'Score 65 should be D');console.assert(getLetterGrade(45) === 'F', 'Score 45 should be F');All five assertions pass — one for each grade range. - 3
Step 3 — Write Edge Case Tests
Now we test the boundary values — the exact scores where grades change:console.assert(getLetterGrade(90) === 'A', 'Boundary: 90 should be A');console.assert(getLetterGrade(89) === 'B', 'Boundary: 89 should be B');console.assert(getLetterGrade(0) === 'F', 'Boundary: 0 should be F');console.assert(getLetterGrade(100) === 'A', 'Boundary: 100 should be A');These boundary tests are crucial because off-by-one errors — using>instead of>=, for example — are among the most common bugs.Boundary tests confirm grades transition correctly at 90, 80, 70, and 60. - 4
Step 4 — Write Error Case Tests
Finally, we consider unusual inputs. What if someone passes a negative score or a score above 100? Our current function doesn't handle these, which the tests reveal:console.assert(getLetterGrade(-5) === 'F', 'Negative score should be F');This test passes because our function returns 'F' for anything below 60. But we might want to add input validation in the future.Error case tests reveal how the function handles unexpected inputs and may inspire improvements. - 5
Step 5 — Interpret Results
If all assertions pass, the console remains silent — no news is good news. If any assertion fails, you'll see a red error message in the console, like:Assertion failed: Boundary: 90 should be AThis tells you exactly which test failed, so you can fix the bug quickly.A complete test suite for getLetterGrade now covers normal, edge, and error cases — giving you high confidence the function works.
SECTION 7
Strengths & Limitations of Assertion-Based Testing
Assertion-based testing is a powerful tool, but like any tool, it has both strengths and limitations. Understanding these will help you use assertions effectively and know when to reach for more advanced testing approaches.
| Aspect | Strengths ✓ | Limitations ✗ |
|---|---|---|
| Simplicity | Easy to write and understand; no external libraries needed for basic assertions. | For complex comparisons (like comparing objects or arrays), simple === doesn't work. |
| Speed | Tests run in milliseconds, so you can run them every time you save your code. | console.assert() doesn't stop execution — later tests may run with corrupted state. |
| Bug Detection | Catches regressions immediately when you change code, preventing old bugs from returning. | Tests can only check scenarios you think of. Unknown edge cases remain untested. |
| Documentation | Test cases serve as living documentation showing how a function is intended to be used. | console.assert() provides minimal output — no summary, no count of pass/fail. |
| Scope | Perfect for testing pure functions (functions that return a value based only on their inputs). | Can't easily test user interface interactions, network requests, or visual output. |
Strengths and limitations of assertion-based testing in JavaScript
KEY TAKEAWAY
SECTION 8
Connection to Professional Testing Frameworks
The conceptual assertions you've learned here are the foundation for every professional testing framework. When you're ready to level up, tools like Jest, Mocha, and Jasmine provide richer assertion libraries, automatic test discovery, detailed reports, and the ability to test asynchronous code. But the core idea remains the same: call a function, check the result.
| Feature | console.assert() | Jest (Professional) |
|---|---|---|
| Syntax | console.assert(x === 5) | expect(x).toBe(5) |
| Output | Error in console only on failure | Detailed pass/fail report with colors |
| Object comparison | Not supported natively | expect(obj).toEqual({a: 1}) |
| Setup required | None — built into every browser | Install via npm, configuration file |
| Best for | Learning, quick checks, small projects | Team projects, production code, CI/CD |
Comparing basic assertions to a professional testing framework
Notice how Jest's expect(x).toBe(5) reads almost like English: "expect x to be 5." This is the same concept as console.assert(x === 5) — just wrapped in a more descriptive and powerful syntax. Once you understand assertions conceptually, transitioning to Jest or any other framework feels natural because you already know the core pattern: arrange, act, assert.
Looking Ahead
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
console.log(). What specific advantage does an assertion provide that a log statement does not?Show answer
console.log() simply prints a value, leaving it to the human developer to compare it against the expected result. With 50 functions, you'd need to manually verify 50 outputs every time you change code. Assertions scale because they only require your attention when something breaks. They turn testing from a manual, error-prone task into an automated, reliable process.Question 2: BASIC
function double(n) { return n * 2; }, write three assertion statements using console.assert() that test this function with a positive number, zero, and a negative number.Show answer
console.assert(double(5) === 10, 'double(5) should return 10'); tests a normal positive input. console.assert(double(0) === 0, 'double(0) should return 0'); tests the edge case of zero. console.assert(double(-3) === -6, 'double(-3) should return -6'); tests a negative number. Each assertion uses strict equality (===) and includes a descriptive error message that identifies which test failed.Question 3: INTERMEDIATE
function max(a, b) { if (a > b) return a; return b; } and claims it works perfectly. Write a set of at least four assertion-based test cases that would thoroughly test this function. Include normal cases, edge cases, and at least one error case. For each test, explain why you chose that particular input.Show answer
console.assert(max(3, 7) === 7, 'max(3,7) should be 7'); — a normal case where b is larger. (2) console.assert(max(10, 2) === 10, 'max(10,2) should be 10'); — a normal case where a is larger. (3) console.assert(max(5, 5) === 5, 'max(5,5) should be 5'); — an edge case where both values are equal. This tests the boundary between the if-branch and the else-branch. (4) console.assert(max(-3, -7) === -3, 'max(-3,-7) should be -3'); — an edge case with negative numbers. Each test targets a different scenario because a function could pass one but fail another if the logic has a subtle flaw.Question 4: APPLIED
function isValidPassword(pw) { return pw.length >= 8; } Your teammate later changes it to also require at least one digit. Write assertion-based test cases that would have caught the change in behavior — meaning, tests that would pass before the change but might fail after, alerting the team that something changed.Show answer
console.assert(isValidPassword('abcdefgh') === true, 'Eight letters should be valid'); would pass because the password is 8 characters. After the teammate adds the digit requirement, this same test would fail because 'abcdefgh' contains no digit — and that failure is exactly the point. The test catches the change in behavior. Other useful tests: console.assert(isValidPassword('short') === false, 'Short password should be invalid'); tests below-minimum length. console.assert(isValidPassword('abc12345') === true, 'Eight chars with digits should be valid'); tests a password that satisfies both the old and new rules. This scenario demonstrates how test cases protect against unintended side effects — also known as regressions.Question 5: CRITICAL THINKING
Show answer
function divide(a, b) { return a / b; } and you test with console.assert(divide(10, 2) === 5) and console.assert(divide(9, 3) === 3). Both pass, but you never tested divide(5, 0), which returns Infinity — a bug. Tests reduce the likelihood of bugs and catch regressions, but they cannot prove the absence of all bugs. This is why developers write as many test cases as practical, focusing on edge cases and error cases, while understanding that testing improves confidence without providing absolute certainty.SUMMARY
Lesson Summary
Summary
A test case is a small piece of code that verifies a function produces the correct output for a specific input. The core mechanism is the assertion — a statement that checks whether a condition is true and reports an error if it's false. In JavaScript, you can use the built-in console.assert() method or write your own assertion function. Every test case follows the Arrange-Act-Assert pattern: set up your inputs, call the function, and check the result using strict equality (===).
A strong test suite covers three categories: normal cases for typical inputs, edge cases for boundary values like zero and negatives, and error cases for invalid or unexpected inputs. While assertion-based testing cannot prove a function is completely bug-free, it dramatically increases confidence in your code and catches regressions — bugs that reappear when you modify existing code. These skills form the foundation for professional testing frameworks like Jest that you'll encounter as you advance.