COMPUTER PROGRAMMING 1 (JAVASCRIPT) • DEBUGGING, TESTING & CODE QUALITY

Writing Test Cases — I can write simple test cases (inputs/expected outputs) for a function and include edge cases.

Learn to verify your code works correctly by defining what goes in, what should come out, and what could go wrong.

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'
Grace Hopper's team found an actual moth stuck in a relay of the Harvard Mark II computer. They taped it into the logbook with the note "First actual case of bug being found." The term debugging became part of computing culture.
1979
Structured Testing Emerges
Glenford Myers published The Art of Software Testing, arguing that testing should be a systematic discipline rather than an afterthought. He emphasized that the purpose of a test is to find failures.
1999
Unit Testing Goes Mainstream
Kent Beck released JUnit, a framework that let programmers write small, automated test cases for individual functions. This idea of unit testing spread rapidly and eventually reached JavaScript.
2014–Now
JavaScript Testing Matures
Tools like Jest, Mocha, and Vitest made it easy to write and run test cases in JavaScript. Today, writing test cases is considered a core skill for every developer, not just specialists.

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.

1

Input → Output Thinking

Every test case starts by choosing specific input values and deciding what the function should return. You're defining correct behavior before you even check the code.
2

Normal Cases First

Start with typical, everyday inputs — the situations your function will handle most often. These normal cases (also called "happy path" tests) confirm the basic logic is correct.
3

Edge Cases Matter

An edge case is an unusual or extreme input — empty strings, zero, negative numbers, or very large values. These are where bugs love to hide.
4

One Test, One Behavior

Each test case should check exactly one thing. If a test fails, you want to know immediately which behavior broke, so keep each test focused.
5

Descriptive Names

Name each test so another person (or future-you) can understand what it checks without reading the code. For example: "returns 0 when both inputs are 0" is much better than "test1."
✦ KEY TAKEAWAY
Think of test cases like a teacher's answer key. Before the teacher hands out an exam, they already know the correct answers. A test case is your answer key for your function — you decide the right answer first, then check if your code agrees. Edge cases are the tricky bonus questions that catch students who only studied the easy stuff.

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.

A test case flows from left to right: choose your input, run it through the function, then compare the actual output to the expected output you predetermined.

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:

MANUAL TEST PATTERN
console.log(double(5)); // Expected: 10
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
console.assert(double(5) === 10, 'double(5) should be 10');
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
function test(name, actual, expected) { if (actual === expected) { console.log(`✓ PASS: ${name}`); } else { console.log(`✗ FAIL: ${name}`); console.log(` Expected: ${expected}, Got: ${actual}`); } }
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 ==?
In JavaScript, the triple equals === 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.

This diagram shows five major categories of edge cases. When writing tests, ask yourself: what happens with zero or negative numbers? With empty strings? With empty arrays? At boundary values?

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
Whenever your function has a condition like 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.

Writing Test Cases for getLetterGrade(score)
1
Step 1 — Start with Normal CasesPick 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 CasesTest 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 ValuesWhat 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 TestsYou 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
Writing test cases is like checking a fence for holes. First, you walk along the normal parts to make sure the fence is standing (normal cases). Then you check every post — where sections connect — because that's where gaps appear (boundary cases). Finally, you check the corners and ends, because those are the most exposed (edge cases).

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.

Comparison of four testing approaches, from simplest to most powerful
ApproachStrengthsLimitations
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() functionClear 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.
🔧 CHOOSING YOUR APPROACH
For this course, focus on the custom test() function approach. It gives you clear pass/fail feedback without needing to install anything. Think of it as training wheels for professional testing — you'll learn the right habits now and seamlessly transition to frameworks like Jest later.

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.

How current skills map to professional testing practices
What You're Learning NowProfessional Version
Writing test cases with inputs and expected outputsUnit testing — testing individual functions in isolation using frameworks like Jest
Checking edge cases manuallyProperty-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 === expectedAssertion libraries — provide rich matchers like toBe(), toEqual(), toThrow(), and toContain()
Deciding what to test by intuitionCode 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

PROBLEM 1 — CONCEPTUAL
In your own words, explain the difference between a normal test case and an edge case. Why is it important to include both when testing a function?
PROBLEM 2 — BASIC CALCULATION
A function called 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.
PROBLEM 3 — INTERMEDIATE
For the same 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.
PROBLEM 4 — APPLIED
You are building a function called 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.
PROBLEM 5 — CRITICAL THINKING
A classmate writes the following function and declares "it works" after testing it with 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.

Varsity Tutors • Computer Programming 1 (JavaScript) • Writing Test Cases