Unit Testing

Reviewed & published by Brayan K

By the end of this lesson you'll be able to write clear, automated unit tests in C++ using the AAA pattern, cover the edge cases where bugs hide, and read tests written in real frameworks like GoogleTest and Catch2 — so you can change code without breaking it.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A unit test is the smoke alarm for one room of your house. You don't wait for the whole building to burn down — you fit a small, cheap sensor in each room that goes off the instant that room has a problem. A unit test is that sensor for one function: it watches a single, isolated piece of behaviour and screams the moment a change breaks it. A houseful of alarms (a test suite) means you can renovate one room — refactor your code — and trust the rest will warn you if you knock something loose.

1. The AAA Pattern

Every good unit test has the same three-part shape, called AAA: Arrange the inputs, Act by calling the thing under test exactly once, then Assert that the result is what you expected. Keeping that order makes a test read top to bottom like a tiny story, so anyone can see what is being checked at a glance. The examples in this lesson use a six-line test harness — a helper called check(...) that prints PASS or FAIL — so the tests actually run here in the editor. Read this worked example first, then run it.

#include <iostream>
using namespace std;

// === A tiny test harness (so these tests RUN in the editor) ===
// Real projects use a framework; this 6-line helper does the same job
// for learning: it checks a condition and prints a pass/fail line.
int passed = 0, failed = 0;
void check(bool condition, const string& testName) {
    if (condition) { cout << "  [PASS] " << testName << "\n"; passed++; }
    else           { cout << "  [FAIL] " << testName << "\n"; failed++; }
}

// === The code under test ===
int add(int a, int b) { return a + b; }

int main() {
    // The AAA pattern: every test has three steps.
    // 1) ARRANGE — set up the inputs.
    int x = 2, y = 3;

    // 2) ACT — call the thing you are testing once.
    int result = add(x, y);

    // 3) ASSERT — check the result is what you expect.
    check(result == 5, "add(2, 3) returns 5");

    // A second test follows the exact same shape.
    check(add(-1, 1) == 0, "add(-1, 1) returns 0");   // boundary: cancels to 0
    check(add(0, 0) == 0,  "add(0, 0) returns 0");     // edge: both zero

    // Summary line — a CI server reads the exit code to know if the build broke.
    cout << passed << " passed, " << failed << " failed\n";
    return failed > 0 ? 1 : 0;
}
// ✅ Expected output:
//      [PASS] add(2, 3) returns 5
//      [PASS] add(-1, 1) returns 0
//      [PASS] add(0, 0) returns 0
//    3 passed, 0 failed

Your turn. The program below is almost complete — fill in the three blanks marked ___ using the // 👉 hints, then run it and check the output against the // ✅ Expected output block.

#include <iostream>
using namespace std;

// Same tiny harness — fill in the blanks below, then press "Try it Yourself".
int passed = 0, failed = 0;
void check(bool condition, const string& testName) {
    if (condition) { cout << "  [PASS] " << testName << "\n"; passed++; }
    else           { cout << "  [FAIL] " << testName << "\n"; failed++; }
}

// Code under test:
int square(int n) { return n * n; }

int main() {
    // 🎯 YOUR TURN — replace each ___ then run it.

    // 1) ARRANGE + ACT: call square(4) and store the result
    int result = ___;            // 👉 square(4)

    // 2) ASSERT: check the result equals 16
    check(result == ___, "square(4) returns 16");   // 👉 16

    // 3) Add one more test for the edge case square(0)
    check(square(0) == ___, "square(0) returns 0"); // 👉 0

    cout << passed << " passed, " << failed << " failed\n";
    return failed > 0 ? 1 : 0;

    // ✅ Expected output:
    //   [PASS] square(4) returns 16
    //   [PASS] square(0) returns 0
    //   2 passed, 0 failed
}

2. Naming, Edge Cases & Errors

A test name is documentation: when it fails, the name alone should tell you what broke. A useful convention is thing_condition_expectation — for example safeDivide_byZero_throws. Then aim your tests at the edge cases, the boundary inputs where bugs love to hide: empty containers, zero, negative numbers, and the largest allowed value. To test that bad input throws, wrap the call in try/catch — reaching the catch is the pass.

#include <iostream>
#include <stdexcept>
using namespace std;

int passed = 0, failed = 0;
void check(bool condition, const string& testName) {
    if (condition) { cout << "  [PASS] " << testName << "\n"; passed++; }
    else           { cout << "  [FAIL] " << testName << "\n"; failed++; }
}

// Code under test: integer division that refuses divide-by-zero.
int safeDivide(int a, int b) {
    if (b == 0) throw invalid_argument("divide by zero");
    return a / b;
}

int main() {
    // Good test NAMES read like a sentence: what + condition + expectation.

    // Happy path — the ordinary case.
    check(safeDivide(10, 2) == 5, "safeDivide_10by2_returns5");

    // Edge cases — where bugs hide: zero, negatives, truncation.
    check(safeDivide(0, 5) == 0,   "safeDivide_zeroNumerator_returns0");
    check(safeDivide(-6, 2) == -3, "safeDivide_negativeNumerator_returnsNegative");
    check(safeDivide(7, 2) == 3,   "safeDivide_truncatesTowardZero");  // 7/2 = 3, not 3.5

    // Error path — assert that bad input THROWS. We expect the exception,
    // so reaching the catch is a PASS; reaching the next line is a FAIL.
    try {
        safeDivide(1, 0);
        check(false, "safeDivide_byZero_throws");   // should not get here
    } catch (const invalid_argument&) {
        check(true, "safeDivide_byZero_throws");    // correct: it threw
    }

    cout << passed << " passed, " << failed << " failed\n";
    return failed > 0 ? 1 : 0;
}
// ✅ Expected output:
//      [PASS] safeDivide_10by2_returns5
//      [PASS] safeDivide_zeroNumerator_returns0
//      [PASS] safeDivide_negativeNumerator_returnsNegative
//      [PASS] safeDivide_truncatesTowardZero
//      [PASS] safeDivide_byZero_throws
//    5 passed, 0 failed

Now you try. The clamp function has three branches — below the range, inside it, and above it — and a good suite tests each one. Fill in the three blanks so every branch is covered:

#include <iostream>
using namespace std;

int passed = 0, failed = 0;
void check(bool condition, const string& testName) {
    if (condition) { cout << "  [PASS] " << testName << "\n"; passed++; }
    else           { cout << "  [FAIL] " << testName << "\n"; failed++; }
}

// Code under test: clamp keeps a value inside [low, high].
int clamp(int value, int low, int high) {
    if (value < low) return low;
    if (value > high) return high;
    return value;
}

int main() {
    // 🎯 YOUR TURN — fill in the blanks to cover the three branches.

    // 1) A value INSIDE the range comes back unchanged
    check(clamp(5, 0, 10) == ___, "clamp_insideRange_unchanged");   // 👉 5

    // 2) A value BELOW the range is pulled up to low
    check(clamp(-3, 0, 10) == ___, "clamp_belowRange_returnsLow");  // 👉 0

    // 3) A value ABOVE the range is pulled down to high
    check(clamp(99, 0, 10) == ___, "clamp_aboveRange_returnsHigh"); // 👉 10

    cout << passed << " passed, " << failed << " failed\n";
    return failed > 0 ? 1 : 0;

    // ✅ Expected output:
    //   [PASS] clamp_insideRange_unchanged
    //   [PASS] clamp_belowRange_returnsLow
    //   [PASS] clamp_aboveRange_returnsHigh
    //   3 passed, 0 failed
}

3. How Real Frameworks Look

Our check(...) harness is just a teaching tool. Real projects use a battle-tested framework that gives you readable assertion macros, automatic test discovery, and a tidy results report. The two most common in C++ are GoogleTest and Catch2. You can't run these here (they need their library linked into your build), but notice they are the same AAA pattern you just learned, with nicer syntax.

GoogleTest wraps each test in TEST(Suite, Name) and checks values with macros like EXPECT_EQ:

// === GoogleTest (gtest) — how the pros write the SAME tests ===
// You don't run this here (it needs the gtest library linked in),
// but notice it is the AAA pattern with nicer macros.
#include <gtest/gtest.h>

int add(int a, int b) { return a + b; }

// TEST(TestSuiteName, TestName) defines one test case.
TEST(AddTest, ReturnsSumOfTwoPositives) {
    EXPECT_EQ(add(2, 3), 5);     // EXPECT_* keeps going if it fails
}

TEST(AddTest, HandlesNegatives) {
    EXPECT_EQ(add(-1, 1), 0);
    ASSERT_NE(add(2, 2), 5);     // ASSERT_* stops THIS test on failure
}

// Common assertions:
//   EXPECT_EQ(a, b)      a == b          EXPECT_TRUE(x) / EXPECT_FALSE(x)
//   EXPECT_NE(a, b)      a != b          EXPECT_NEAR(a, b, tol)  for doubles
//   EXPECT_LT/GT/LE/GE   < > <= >=       EXPECT_THROW(stmt, ExType)

// No main() needed — link gtest_main. Run: ./tests  ->  [  PASSED  ] 3 tests.

// ⚠️ No expected-output panel for this one, on purpose.
// GoogleTest has to be installed and linked (g++ test.cpp -lgtest_main
// -lgtest -pthread), so this build cannot run it and its result is not
// machine-checked like the hand-rolled test above.

Catch2 is header-only and even lighter — TEST_CASE("...") names the test in plain English and REQUIRE does the assert:

// === Catch2 — a popular header-only framework ===
// Same idea, even less boilerplate. Run here? No — it needs Catch2 linked.
#define CATCH_CONFIG_MAIN          // generates main() for you
#include <catch2/catch_all.hpp>

int add(int a, int b) { return a + b; }

// TEST_CASE("description", "[tag]") names the test in plain English.
TEST_CASE("add returns the sum", "[math]") {
    REQUIRE(add(2, 3) == 5);      // REQUIRE stops the test on failure
    CHECK(add(0, 0) == 0);        // CHECK reports but keeps going
}

// SECTION lets several cases share the same setup (a built-in fixture):
TEST_CASE("add handles signs", "[math]") {
    SECTION("two negatives")  { REQUIRE(add(-2, -3) == -5); }
    SECTION("mixed signs")    { REQUIRE(add(-2,  5) ==  3); }
}

// Run:  ./tests  ->  All tests passed (4 assertions in 2 test cases).

// ⚠️ No expected-output panel for this one, on purpose.
// Catch2 has to be installed (g++ test.cpp -lCatch2Main -lCatch2), so this
// build cannot run it and its result is not machine-checked like the
// hand-rolled test above.

🔎 Deep Dive: EXPECT vs ASSERT

Most frameworks give you two flavours of every check. The soft one (GoogleTest EXPECT_*, Catch2 CHECK) records a failure but keeps running the rest of the test, so one run shows you every problem. The hard one (ASSERT_* / REQUIRE) stops that test immediately.

Use the hard version when carrying on would crash — for example, after checking a pointer isn't null, before you dereference it. Reach for the soft version everywhere else so a single failing line doesn't hide the next five.

ASSERT_NE(ptr, nullptr);   // stop here if it's null...
EXPECT_EQ(ptr->size(), 3); // ...so this line is safe to run

🔎 Deep Dive: Test Doubles (Stubs & Mocks)

A unit test should test one unit, but real code has dependencies — a clock, a database, a network call. A test double is a stand-in for one of those so your unit can be tested in isolation, fast and repeatably.

A stub returns canned answers: a fake clock whose now() always returns noon, so a test of "is it lunchtime?" is deterministic. A mock goes further and records how it was called, so you can assert "the email sender was called exactly once". In C++ you usually inject a double by coding to an interface (an abstract base class) and passing the fake implementation in during the test.

Common Errors (and the fix)

📋 Quick Reference

GoalGoogleTestCatch2
Define a testTEST(Suite, Name)TEST_CASE("name")
Values equalEXPECT_EQ(a, b)CHECK(a == b)
Equal, stop on failASSERT_EQ(a, b)REQUIRE(a == b)
Boolean trueEXPECT_TRUE(x)CHECK(x)
Doubles nearEXPECT_NEAR(a, b, t)REQUIRE(a == Approx(b))
Expect a throwEXPECT_THROW(s, E)REQUIRE_THROWS_AS(s, E)

Mini-Challenge: Test countVowels

No blanks this time — just a brief and the harness. Write at least four check(...) calls covering a normal word and the edge cases (empty string, no vowels, all vowels), then print the summary line. Run it and confirm 4 passed, 0 failed.

#include <iostream>
using namespace std;

// Harness provided — write the tests yourself below.
int passed = 0, failed = 0;
void check(bool condition, const string& testName) {
    if (condition) { cout << "  [PASS] " << testName << "\n"; passed++; }
    else           { cout << "  [FAIL] " << testName << "\n"; failed++; }
}

// Code under test: count vowels in a lowercase word.
int countVowels(const string& word) {
    int n = 0;
    for (char c : word)
        if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') n++;
    return n;
}

int main() {
    // 🎯 MINI-CHALLENGE: test countVowels
    // Write at least FOUR check(...) calls, using AAA + good names:
    //   1. A normal word, e.g. "hello"            -> 2 vowels
    //   2. The EDGE case of an empty string ""    -> 0 vowels
    //   3. A word with NO vowels, e.g. "rhythm"   -> 0 vowels
    //   4. A word that is ALL vowels, e.g. "aeiou"-> 5 vowels
    // Then print the "X passed, Y failed" summary line.
    //
    // ✅ Expected: 4 passed, 0 failed

    // your tests here


}

🎉 Lesson Complete

Practice quiz

What do the three letters in the AAA test pattern stand for?

  • Assert, Act, Arrange
  • Add, Apply, Assert
  • Arrange, Act, Assert
  • Arrange, Assert, Announce

Answer: Arrange, Act, Assert. AAA = Arrange the inputs, Act by calling the unit under test once, then Assert the result is what you expected.

What is a 'unit' in unit testing?

  • The smallest testable piece of behaviour, usually one function or method
  • A whole program
  • A database table
  • A thread

Answer: The smallest testable piece of behaviour, usually one function or method. A unit is the smallest piece of behaviour you can test alone — usually a single function or one method — with no files, network, or other units.

In GoogleTest, what is the difference between EXPECT_EQ and ASSERT_EQ?

  • No difference
  • ASSERT keeps running; EXPECT stops
  • EXPECT is for doubles only
  • EXPECT keeps running the test on failure; ASSERT stops that test

Answer: EXPECT keeps running the test on failure; ASSERT stops that test. EXPECT_* reports a failure but lets the test continue; ASSERT_* stops that test immediately (use it when continuing would crash).

In Catch2, which macro stops the test immediately on failure?

  • CHECK
  • REQUIRE
  • SECTION
  • EXPECT

Answer: REQUIRE. REQUIRE is Catch2's hard assert that stops the test; CHECK is the soft version that reports but keeps going.

A test always passes even when the code is wrong. A likely cause is:

  • Using = instead of ==, e.g. check(x = 5, ...)
  • Using == instead of =
  • Too many test names
  • Comparing ints

Answer: Using = instead of ==, e.g. check(x = 5, ...). check(x = 5, ...) assigns 5 (a truthy value) instead of comparing. Use == to actually test equality: check(x == 5, ...).

Why is comparing two doubles with == in a test risky?

  • == is illegal for doubles
  • Doubles are always equal
  • Floating-point math is inexact, so 0.1 + 0.2 == 0.3 is false
  • It throws an exception

Answer: Floating-point math is inexact, so 0.1 + 0.2 == 0.3 is false. Floating-point results are inexact. Compare within a tolerance (fabs(a - b) < 1e-9) or use EXPECT_NEAR / Approx.

Which inputs are the 'edge cases' where bugs most often hide?

  • Only large random numbers
  • Empty, zero, negative, and boundary values
  • Only the happy path
  • Only string inputs

Answer: Empty, zero, negative, and boundary values. Bugs cluster at boundaries: empty containers, zero, negatives, and the largest allowed value. Aim tests at those.

To test that bad input throws, where should the pass be recorded?

  • After the call, outside any try
  • Only if no exception is thrown
  • In the destructor
  • When the catch block is reached

Answer: When the catch block is reached. Wrap the call in try/catch; reaching the catch is the pass. Put a fail line right after the call so a silent non-throw is caught.

What is a test double (stub or mock)?

  • A test that runs twice
  • A stand-in for a real dependency so a unit can be tested in isolation
  • A duplicate of the production class
  • A second main() function

Answer: A stand-in for a real dependency so a unit can be tested in isolation. A test double replaces a real dependency (clock, DB, network). A stub returns canned answers; a mock also records how it was called.

Writing the failing test BEFORE the code is known as what?

  • Integration testing
  • Fuzzing
  • Test-Driven Development (TDD)
  • Code coverage

Answer: Test-Driven Development (TDD). Writing the test first is TDD. It forces you to design the interface from the caller's view and guarantees the test fails before you make it pass.

Continue this course

Frequently asked questions

What exactly is a 'unit' in unit testing?

A unit is the smallest piece of behaviour you can test on its own — usually a single function or one method of a class. A unit test calls that one unit with known inputs and asserts on its output, without touching files, networks, or other units. If you find yourself needing a database to run the test, you are writing an integration test, not a unit test.

Why bother — can't I just print values and eyeball them?

Printing checks the code once, by hand, and proves nothing tomorrow. A unit test encodes the expected answer so the machine checks it every time, instantly, and fails loudly the moment a change breaks it. That safety net is what lets you refactor with confidence instead of fear.

What is the difference between EXPECT and ASSERT (or CHECK and REQUIRE)?

Both verify a condition. The EXPECT/CHECK family reports a failure but lets the rest of the test keep running, so you see every problem in one go. The ASSERT/REQUIRE family stops that test immediately — use it when continuing would crash (for example, after checking a pointer is not null before you dereference it).

What is a test double, mock, or stub?

A test double is a stand-in for a real dependency so you can test a unit in isolation. A stub returns canned answers (a fake clock that always says noon); a mock also records how it was called so you can assert on that. You reach for them when the real thing is slow, random, or has side effects — like a network call or the system time.

Should I write the test before or after the code?

Both are valid; writing the test first is Test-Driven Development (TDD). Writing the failing test first forces you to design the interface from the caller's point of view and guarantees the test actually fails before you make it pass. Whichever order you choose, the goal is the same: every behaviour ends up covered by a test that runs automatically.

Related lessons