JavaScript • Debugging and Testing

Writing Test Cases — Write simple test cases for functions using assertions (conceptual)

Learn how assertions let you automatically verify that your JavaScript functions produce the correct output every time.

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 6 figures

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

Programmers tested code by reading printouts and checking results by hand. Bugs were found only when the program visibly crashed or produced obviously wrong output.

1979 • assert() in C Language

The C programming language introduced the assert() macro, letting developers embed automatic checks directly inside their code. This idea influenced every language that followed.

1999 • JUnit & Unit Testing Movement

Kent Beck and Erich Gamma released JUnit for Java, popularizing the practice of writing small, focused test cases for individual functions — a practice called unit testing.

2010s • JavaScript Testing Frameworks

Tools like Mocha, Jasmine, and Jest brought powerful assertion-based testing to JavaScript, making it standard practice for web developers worldwide.

Today • Testing as a Requirement

Modern software teams require test cases before code can be merged. Continuous Integration (CI) systems run all tests automatically on every code change.

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.

1

Test Case

A small block of code that exercises a function with known inputs and compares the actual output to the expected output. Each test targets one specific behavior.
2

Assertion

A statement that evaluates a condition and throws an error if the condition is false. Think of it as a checkpoint: console.assert(2 + 2 === 4) passes because the math is correct.
3

Expected vs. Actual

The expected value is what you believe the function should return. The actual value is what it really returns. A test passes when expected === actual.
4

Edge Case

An unusual or extreme input — like zero, negative numbers, empty strings, or very large values. Good test suites always include edge cases because bugs love to hide there.
5

Test Suite

A collection of related test cases grouped together. Running a test suite gives you a quick report of how many tests passed and how many failed.
✦

KEY TAKEAWAY

Think of assertions like a teacher's answer key. When you grade a student's test, you compare their answer to the correct one. An assertion does the same thing — it compares your function's answer to the correct one. If they match, the function "passes." If not, you know exactly which question (function behavior) went wrong.

SECTION 3

Visual Explanation — The Anatomy of a Test Case

Anatomy of a Test CaseSTEP 1 — ARRANGE (Set Up Inputs)const input1 = 3; const input2 = 5;STEP 2 — ACT (Call the Function)const result = add(input1, input2);STEP 3 — ASSERT (Check the Result)console.assert(result === 8, 'add(3,5) should be 8');✓ PASSresult === 8 → trueNo error thrown.The function works correctly.✗ FAILresult === 8 → falseAssertion error logged.Bug detected — investigate!
This diagram shows the three-step pattern every test case follows: Arrange your inputs, Act by calling the function, and Assert that the result matches your expectation. The bottom shows two possible outcomes: a green pass or a red fail.

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

console.assert(condition,errorMessage)console.assert(condition, errorMessage)

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

functionassert(condition,message)if(!condition)thrownewError(message);function assert(condition, message) { if (!condition) throw new Error(message); }

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()?

You could test by printing results with 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.

Test Case Categories for isEven(n)NORMAL CASESTypical, expected inputsisEven(4)→ trueisEven(7)→ falseisEven(10)→ trueThese confirm thefunction handleseveryday inputscorrectly.EDGE CASESBoundary / unusual inputsisEven(0)→ trueisEven(-2)→ trueisEven(1)→ falseThese test boundarieslike zero, negatives,and the smallestmeaningful values.ERROR CASESInvalid / unexpected inputsisEven('hi')→ falseisEven(null)→ falseisEven(3.5)→ falseThese verify thefunction handles badinput gracefullyinstead of crashing.A strong test suite includes all three categories to maximize confidence in your code.
The three categories of test cases shown above — normal, edge, and error — together form a comprehensive test suite for the 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. 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. 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. 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. 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. 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 A This 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.

AspectStrengths ✓Limitations ✗
SimplicityEasy to write and understand; no external libraries needed for basic assertions.For complex comparisons (like comparing objects or arrays), simple === doesn't work.
SpeedTests 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 DetectionCatches regressions immediately when you change code, preventing old bugs from returning.Tests can only check scenarios you think of. Unknown edge cases remain untested.
DocumentationTest 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.
ScopePerfect 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

Assertions are like a smoke detector in your house. They won't prevent a fire (bug) from starting, and they can't detect every possible danger — but when something does go wrong, they alert you immediately so you can fix it before it spreads. For more advanced detection, you'd upgrade to a full security system (a testing framework like Jest), but a smoke detector is a great first line of defense.

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.

Featureconsole.assert()Jest (Professional)
Syntaxconsole.assert(x === 5)expect(x).toBe(5)
OutputError in console only on failureDetailed pass/fail report with colors
Object comparisonNot supported nativelyexpect(obj).toEqual({a: 1})
Setup requiredNone — built into every browserInstall via npm, configuration file
Best forLearning, quick checks, small projectsTeam 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

Professional developers also use techniques like Test-Driven Development (TDD), where you write the test cases before writing the function. This forces you to think clearly about what the function should do before you start coding. It's a powerful discipline that builds on everything you're learning now.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

In your own words, explain why writing test cases with assertions is more reliable than manually checking output with console.log(). What specific advantage does an assertion provide that a log statement does not?
Show answer
An assertion automatically checks whether a condition is true and alerts you when it's false — you don't have to read the output yourself. A 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

Given a function 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
Here are three proper assertions: 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

A classmate writes the function 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
A thorough test suite would include: (1) 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

You're building a simple password validator function: 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
Before the change, 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

Consider this claim: "If all my test cases pass, my function is bug-free." Is this statement true or false? Explain your reasoning, and describe a scenario where all tests pass but the function still contains a bug.
Show answer
This statement is false. Passing test cases only prove that the function works correctly for the specific inputs you tested — they do not guarantee correctness for every possible input. For example, suppose you have a function 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.

Varsity Tutors • JavaScript • Writing Test Cases — Write simple test cases for functions using assertions (conceptual)