Skip to main content

    Lesson • Intermediate Track

    Unit Testing

    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.

    What You'll Learn

    • Structure every test with the AAA pattern (Arrange, Act, Assert)
    • Write runnable assertion-based tests with a tiny test harness
    • Name tests so a failure tells you exactly what broke
    • Cover the edge cases — empty, zero, negative, boundaries — where bugs hide
    • Read real-framework tests: GoogleTest TEST/EXPECT_EQ and Catch2 TEST_CASE/REQUIRE
    • Know what test doubles (stubs and mocks) are and when to use them

    💡 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.

    Worked example: AAA with a tiny harness

    Read every comment, run it, and check the output matches.

    Try it Yourself »
    C++
    #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
    ...

    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.

    🎯 Your turn: assert on square()

    Fill in the ___ blanks, then check your output against the expected lines.

    Try it Yourself »
    C++
    #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:
    ...

    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.

    Worked example: edge cases & exception tests

    See happy path, boundary cases, and a 'should throw' test.

    Try it Yourself »
    C++
    #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
    ...

    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:

    🎯 Your turn: cover every branch of clamp()

    Fill in the expected values so all three branches are tested.

    Try it Yourself »
    C++
    #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 — 
    ...

    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:

    Worked example: GoogleTest (read-only)

    The same add() tests, written the way the pros do.

    Try it Yourself »
    C++
    // === 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_
    ...

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

    Worked example: Catch2 (read-only)

    Same tests again, with SECTION-based fixtures.

    Try it Yourself »
    C++
    // === 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 report
    ...

    🔎 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)

    • Test always passes, even when the code is wrong: you used = instead of ==. check(x = 5, ...) assigns 5 (truthy) and never compares. Write check(x == 5, ...).
    • Comparing doubles with == fails mysteriously: floating-point maths is inexact, so 0.1 + 0.2 == 0.3 is false. Compare within a tolerance: fabs(a - b) < 1e-9 (or EXPECT_NEAR).
    • A "should throw" test passes by accident: if you forget the check(false, ...) line after the call, the test passes even when nothing throws. Always put a fail line right after the call inside the try.
    • Tests pass alone but fail together: they share mutable global state, so one test's changes leak into the next. Give each test a fresh starting state instead of reusing one object.
    • GoogleTest: "undefined reference to `main`": you didn't link gtest_main (which supplies main()), or you wrote your own. Link gtest_main, or call RUN_ALL_TESTS() from your own main.

    📋 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)

    Frequently Asked Questions

    Q: 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.

    Q: 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.

    Q: 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).

    Q: 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.

    Q: 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.

    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.

    🎯 Mini-Challenge: write the tests yourself

    Cover the happy path plus three edge cases using AAA + good names.

    Try it Yourself »
    C++
    #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
    ...

    🎉 Lesson Complete

    • ✅ Every test follows AAA: Arrange the inputs, Act once, Assert the result
    • ✅ A test harness turns a condition into a PASS/FAIL line so tests run automatically
    • ✅ Name tests thing_condition_expectation so a failure explains itself
    • ✅ Aim at edge cases — empty, zero, negative, boundaries — and test that bad input throws
    • ✅ Real frameworks (GoogleTest EXPECT_EQ, Catch2 REQUIRE) are the same pattern, nicer syntax
    • EXPECT/CHECK keep going; ASSERT/REQUIRE stop; test doubles isolate a unit
    • Next lesson: Custom Iterators — make your own types work with range-based for

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service