SovranCode
Modern C++ Lesson 43
Course contents50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
Modern C++50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
PREVIOUS LESSONC++20 Concepts and Template Constraints
NEXT LESSONC++ Sanitizers: Find Memory and Undefined Behavior Bugs
C++ foundations · Lesson 43 About 49 min with practice

C++ Unit Testing: Normal and Boundary Cases

Learn C++ unit testing with a pure function, named normal and boundary cases, expected versus actual results, and a test runner that fails honestly without relying on assert.

Before you begin

Know functions, optional, vectors and structs. The first test harness uses only the standard library so you can understand the idea before adding a framework.

What you will learn

  • Write a small observable contract before its tests.
  • Separate pure domain logic from terminal input.
  • Test normal, exact-boundary and invalid cases.
  • Compare expected and actual results without treating zero as missing.
  • Return a failure status when any check fails.

A test asks one clear question about behavior

Looking at a program's output once is useful, but it is easy to forget a boundary when the code changes. An automated test runs a repeatable check and reports whether the actual result matches an expected result. A unit test focuses on a small piece of behavior, often a function. You do not need to start with a large testing framework to understand the central idea: arrange input, call the function, compare its answer.

Our example answers how many seats remain. Capacity must be zero through twenty. Booked seats must be zero through capacity. For valid input the result is capacity minus booked; otherwise there is no value. Zero remaining seats is a valid answer, not failure. Writing this contract first gives you a reason for each expected value. Tests should not merely copy whatever the current implementation happens to return.

The function is pure for this exercise: its result depends on its arguments, and it does not print, read input, access files or change outside state. This makes tests small and repeatable. A terminal program can call the same function after parsing user input. You can then test input handling separately. Separating responsibilities is often a more valuable first step than installing a framework around a function that does everything at once.

Three seat-count checks show capacity five with zero booked expecting five, five booked expecting a present zero, and six booked expecting no value. Each case compares a pure function's actual result with its independently stated expectation.
Normal, boundary and invalid cases answer different questions. A test must compare the actual value or absence with the expected result; simply checking truthiness of an integer would lose a valid zero.
C++ PRACTICE RUNNER

Keep the rule separate from input and output

The caller can observe an optional value without the function depending on a terminal or clock.

#include <iostream>
#include <optional>
std::optional<int> remainingSeats(int capacity, int booked) {
    if (capacity < 0 || capacity > 20 || booked < 0 || booked > capacity)
        return std::nullopt;
    return capacity - booked;
}
int main() {
    const auto result = remainingSeats(5, 2);
    if (result) std::cout << "Remaining: " << *result << '\n';
    else std::cout << "Rejected\n";
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Remaining: 3

After editing inputs, calculate a new prediction; this reference describes only the starter.

Arrange, act and check one result

Arrange means choose a specific input and an expected answer. For capacity five and booked two, three seats should remain because five minus two is three. Act means call remainingSeats with those arguments. Check means compare the returned optional with an optional containing three. These names are simple reminders about responsibilities, not a requirement to write three separate functions for every tiny check.

Give each case a useful name such as partly booked or full. When a check fails, the name points you toward the rule that changed. A message saying failed is less helpful than one naming the relevant case. For richer values, a framework can display expected and actual data. Our starter runner stays small, but the same principle applies: report enough information to locate the broken contract without burying it in unrelated output.

Be careful with optional checks. An optional holding zero has a value, and comparing it with optional<int>{0} should succeed. A missing optional is a different state. Do not convert the contained integer to bool to decide whether the whole operation succeeded. That would reject the full-capacity case even when the function correctly returned zero remaining seats. This is why precise expectations matter more than a green-looking output label.

  • Arrange capacity 5, booked 2 and expected value 3.
  • Call remainingSeats once for that case.
  • Compare the complete optional result with the expectation.
  • Report a named failure if they differ, and return a failing test status.
C++ PRACTICE RUNNER

Check zero and absence as different outcomes

Optional equality compares both presence and the contained value. This complete program uses explicit checks.

#include <iostream>
#include <optional>
std::optional<int> remainingSeats(int capacity, int booked) {
    if (capacity < 0 || capacity > 20 || booked < 0 || booked > capacity)
        return std::nullopt;
    return capacity - booked;
}
int main() {
    const bool full = remainingSeats(5, 5) == std::optional<int>{0};
    const bool invalid = remainingSeats(5, 6) == std::nullopt;
    if (!full || !invalid) { std::cout << "Failed\n"; return 1; }
    std::cout << "Two checks passed\n";
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Two checks passed

After editing inputs, calculate a new prediction; this reference describes only the starter.

Test the edges, not only the middle

A normal case such as five and two shows the main calculation. Boundary cases test the exact edges: zero capacity, capacity twenty, zero booked and every seat booked. Invalid cases sit just outside the rules: negative capacity, twenty-one capacity, negative booked or more booked than capacity. If you only test one ordinary case, an incorrect less-than versus less-than-or-equal condition can remain unnoticed.

Include the values immediately beside a boundary when they are meaningful. If capacity twenty is accepted, test twenty-one as rejected. If zero booked is accepted, test minus one as rejected. Name the reason for each case. You do not have to enumerate every integer to build useful evidence, but you should be able to explain which categories of behavior your set covers and which remain outside this exercise.

Do not make expected results depend on calling the same implementation again. A test that computes expected by using the function under test cannot reliably challenge that function. Small literal expected answers are appropriate here because the contract is simple. For larger problems, an independent model or a property may help. Keep independence in mind: an automated comparison is only as useful as the expectation it compares against.

C++ PRACTICE RUNNER

Make a failing case identifiable

This passing check shows the same failure path the lab runner uses. Changing the expected result should make the process fail.

#include <iostream>
#include <optional>
std::optional<int> remainingSeats(int capacity, int booked) {
    if (capacity < 0 || capacity > 20 || booked < 0 || booked > capacity)
        return std::nullopt;
    return capacity - booked;
}
int main() {
    const auto expected = std::optional<int>{3};
    const auto actual = remainingSeats(5, 2);
    if (actual != expected) {
        std::cout << "Failed: partly booked\n";
        return 1;
    }
    std::cout << "Passed: partly booked\n";
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Passed: partly booked

After editing inputs, calculate a new prediction; this reference describes only the starter.

A test runner must tell automation when it failed

Return status zero only when all required checks pass. Return a nonzero status when at least one check fails. A build script or later continuous-integration job can then stop on failure. Printing Failed while returning zero is a misleading interface for automation. The lab prints named failures, counts passed cases, and uses its final status to express whether the test run succeeded as a whole.

The C assert macro can be disabled when NDEBUG is defined. It can be useful for suitable internal assertions, but do not rely on it as the only mechanism in a test runner that must remain active in every build configuration. This lesson uses explicit comparisons and return values. Later, a test framework can add discovery, richer diagnostics and reporting without changing the need for an honest success or failure result.

A passing suite is evidence for the cases that ran, not proof that every possible use is correct. Tests of a pure function do not automatically verify the terminal parser, file loader or website integration. Keep those layers visible. A flaky test that depends on a real clock or external service can also hide regressions. Make small domain tests deterministic, then add appropriately isolated integration checks for the surrounding systems.

Prove that your tests can notice a mistake

A useful experiment is to temporarily change the implementation from capacity minus booked to capacity plus booked. The partly booked and full cases should fail. Restore the correct subtraction afterward. This is a small mutation check: it asks whether your test set detects a plausible bug. Do not keep a deliberately wrong implementation in the final working source or mark a failed test run as a successful verification.

If a change produces unexpected failures, read the named cases before rewriting their expected values. A failure may mean the implementation broke, the expectation was wrong, or the contract deliberately changed. Decide which explanation is supported by the requirements. Updating expected answers merely to make the output green removes the protection the test was meant to provide. Preserve the failing case alongside its reason and repair.

Write a ten-case seat-rule test suite

Implement remainingSeats and a table of ten named cases in the reference solution's contract: empty capacity, no bookings, full, partly booked, maximum empty, maximum full, negative capacity, beyond the capacity limit, negative bookings and overbooked. Use optional expected answers so a present zero is different from absence. The program takes no standard input; the test table is its deliberate input fixture.

Run every case, print Failed with its name for each mismatch, then print Passed followed by the number out of ten. Return one if any case failed. Verify the passing suite, try the subtraction-to-addition mutation, and restore the code. Record both observations in your lab notes. A screenshot of a success message is not enough if the runner cannot reject an incorrect implementation or silently skips checks under release flags.

LOCAL TERMINAL COMMANDS

Build one complete safe example

Choose an installed compiler. These commands run locally, not in the source editor. The in-page Run button requires the configured compiler sandbox. Diagnostic fixtures and multi-file projects use their separate instructions.

macOS / Linux:
clang++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
# Or use g++ with the same options.
./lesson

Windows Developer Command Prompt:
cl /std:c++20 /EHsc /W4 main.cpp /Fe:lesson.exe
lesson.exe
APPLY THE CONCEPT

Independent lab

Build a deterministic table-driven test runner for one pure optional-returning rule.

Acceptance checklist

  • Implement capacity 0..20 and booked 0..capacity; return optional remaining seats or nullopt.
  • Use all ten named normal, boundary and invalid cases with independent literal expectations.
  • Compare optional values directly so zero is not confused with absence.
  • Run all cases with explicit checks, print each failed name and Passed: N/10, and return 1 on any failure.
  • Verify checks still run with NDEBUG; try a subtraction-to-addition mutation and restore the correct rule.
Need a hint?
  1. A Case struct can hold capacity, booked, expected and a short name.
  2. Do not call the production function to construct the expected answer.
  3. The test runner is the program; it does not need cin or a real booking service.
C++ PRACTICE RUNNER

Write and run your lab

Build your own solution before revealing the reference. Use the input field for normal, boundary and invalid cases; this runner executes your program but does not grade it.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>

int main() {
    // Implement the acceptance checklist above.
    return 0;
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Your lab evidence and notes

Record predictions, actual output, commands and the cause of each repair. Code is plain text: this scratchpad does not execute or grade C++.

Loading local notes…

Review your acceptance checklist and quiz explanations before marking this lesson complete. This is self-review, not a grade or certification.

Reveal the explained lab solution
C++ PRACTICE RUNNER

Reference lab solution

Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.

#include <iostream>
#include <vector>
#include <optional>
std::optional<int> remainingSeats(int capacity, int booked) {
    if (capacity < 0 || capacity > 20 || booked < 0 || booked > capacity)
        return std::nullopt;
    return capacity - booked;
}
struct Case {
    int capacity;
    int booked;
    std::optional<int> expected;
    const char* name;
};
int main() {
    const std::vector<Case> cases{
        {0, 0, 0, "empty capacity"}, {5, 0, 5, "no bookings"},
        {5, 5, 0, "full"}, {5, 2, 3, "partly booked"},
        {20, 0, 20, "maximum empty"}, {20, 20, 0, "maximum full"},
        {-1, 0, std::nullopt, "negative capacity"},
        {21, 0, std::nullopt, "beyond capacity limit"},
        {5, -1, std::nullopt, "negative bookings"},
        {5, 6, std::nullopt, "overbooked"}
    };
    std::size_t passed{};
    for (const auto& item : cases) {
        if (remainingSeats(item.capacity, item.booked) == item.expected) ++passed;
        else std::cout << "Failed: " << item.name << '\n';
    }
    std::cout << "Passed: " << passed << '/' << cases.size() << '\n';
    return passed == cases.size() ? 0 : 1;
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Expected output

Passed: 10/10

The table states expected behavior independently of the implementation, including present zero and missing outcomes.

Every comparison runs explicitly, so defining NDEBUG does not disable these checks. The exit status communicates failure to automation.

A mutation that changes subtraction to addition must trigger named failures. Passing these cases still does not prove the parser or a whole booking application correct.

Review before moving on

Explain the lesson's objectives in your own words, compare your lab evidence with the acceptance checklist, and keep the first failing case alongside your repair. Completing a page is not proof that every C++ feature or input is supported.

  • Write a small observable contract before its tests.
  • Separate pure domain logic from terminal input.
  • Test normal, exact-boundary and invalid cases.
  • Compare expected and actual results without treating zero as missing.
  • Return a failure status when any check fails.

References and further reading

Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.

  • C++ draft: optional comparisons
  • C++ draft: assert macro
KNOWLEDGE CHECK

Check your understanding

Answer all five questions, then review the explanations. Answers are not saved or synced; completion is a separate self-review choice.

01What makes a domain unit test repeatable here?
02Is optional<int>{0} the same as nullopt?
03What status should a failed test run return?
04Why not rely only on assert for this runner?
05Why temporarily change subtraction to addition?
PREVIOUS LESSONC++20 Concepts and Template Constraints
NEXT LESSONC++ Sanitizers: Find Memory and Undefined Behavior Bugs
ON THIS PAGELearning objectivesA test asks one clear question about behaviorArrange, act and check one resultTest the edges, not only the middleA test runner must tell automation when it failedProve that your tests can notice a mistakeWrite a ten-case seat-rule test suiteIndependent labReviewKnowledge checkReferences
Course overview