Introduction to Software Engineering · Chapter 09 · Week 11 (Sprint 2)

Lab-09: Software Testing and Quality Assurance

Sprint 2 turns the business rules of ITC Club Hub into evidence. Your team writes a one-page test plan, designs equivalence and boundary test cases for registration, cancellation and check-in, implements them as GoogleTest tests with an injected clock, measures statement and branch coverage in CI, and runs the quality-assurance loop: a peer review with a checklist, a static-analysis run and three bug reports. The challenge adds an exploratory testing session and an acceptance test run with the client.

GoogleTest 1.15 · gcov · gcovr clang-tidy · cppcheck C++20 · CMake 3.28 · CTest · GitHub Actions ≈ 7 hours per team 5 tasks + 1 challenge
How to work through this lab. Do the tasks in order; each builds on the previous. Read the Goal, follow the Steps, compare with the Expected output, and tick the Acceptance checklist (saved in your browser). Open a hint only when stuck for more than 10 minutes. This is team work: split the tasks, but make sure every member authors commits in tests/ and in lab09/, so that individual contributions are visible in the Git history.
0

Setup

25 min
  1. Tools. Verify the toolchain from Lab-08 and add the test and analysis tools. gcov ships with GCC; gcovr is a Python package. MSVC does not produce gcov data: Windows users measure coverage in WSL (Ubuntu) or rely on the CI coverage job of Task 4, and can still run all tests with MSVC.
    git switch main && git pull && git switch -c lab-09
    cmake --version            # 3.28 or later
    g++ --version              # GCC 13 or later (or clang++ 17 or later)
    python -m pip install --user gcovr     # or: pipx install gcovr
    gcovr --version
    clang-tidy --version       # any current release (LLVM, or Visual Studio 2022 "C++ Clang tools")
    cppcheck --version         # 2.13 or later
    gh --version               # optional: GitHub CLI to file issues from the terminal
    # WSL / Ubuntu in one line:
    # sudo apt install build-essential cmake gcovr clang-tidy cppcheck
  2. Inputs from earlier labs. This lab reuses the acceptance criteria and traceability matrix of Lab-05, the state machine of Lab-06, the Sprint 2 backlog of Lab-07 and the C++ code and CI pipeline of Labs 07 and 08. If one of them is incomplete, use the fallback; do not stop to repair the earlier lab. If your file names differ, keep yours and name them in lab09/README.md.
    InputFileWhat you need from itFallback if missing
    Acceptance criterialab05/user-stories.mdGherkin scenarios of the register, cancel and check-in storiesWrite one Given/When/Then per rule of the case-study brief below into lab09/test-design.md.
    Traceability matrixlab05/traceability-matrix.csvFR ids, story ids, an empty test columnUse the fallback ids of the brief: FR-05 create event, FR-11 register, FR-12 cancel, FR-13 check-in, FR-14 waiting list (Should).
    Registration state machinelab06/ (state machine diagram)States and transitions of a RegistrationRegistered → Cancelled (cancel), Registered → CheckedIn (check-in), Waitlisted → Registered (seat freed, Should).
    Sprint 2 backlog and definition of donelab07/ and the GitHub Project boardWhich stories are in Sprint 2; the team's DoDStories: register, cancel, check-in, attendance list. DoD: reviewed PR, tests pass in CI, acceptance criteria met.
    C++ codeinclude/clubhub/, src/, tests/RegistrationService with registerStudent, cancel, checkIn; EventRepository, RegistrationRepositoryThe fallback header in step 5; implement the rules of the brief before Task 3.
    CI pipeline.github/workflows/ci.yml (Lab-08)Build matrix GCC + Clang running ctest; protected mainThe coverage job of Task 4 also works on its own; protect main with one required review.
  3. Case-study brief. The box below holds everything about the product that this lab needs. Where a step says "the brief", it means this box.
    Case-study brief: ITC Club Hub, the rules under test
    Product
    ITC Club Hub for the student clubs of the Institute of Technology of Cambodia. Club leaders register a club, an administrator approves it; approved clubs publish events (title, date and time, location, capacity, description); students browse, register, cancel and get a reminder; organisers check students in at the door and see attendance; administrators see a monthly report. The C++20 core (domain model, rules, CLI, CSV storage) is what you test.
    Register (FR-11)
    The event must exist and must not have started (now < start). The student must not already hold a Registered or CheckedIn registration for it; a student who cancelled may register again. Seats taken (Registered + CheckedIn) must be below capacity, otherwise the request is rejected with EventFull (or Waitlisted once the Should story FR-14 is built).
    Create event (FR-05)
    Capacity is at least 1; only a club with status Approved may publish.
    Cancel (FR-12)
    Only a Registered registration can be cancelled, and only while now ≤ start − 24 h (the deadline itself is still allowed). Cancelling frees the seat.
    Check-in (FR-13)
    Only a Registered student can be checked in (not Cancelled, Waitlisted or already CheckedIn). Fallback window, to confirm with your Product Owner: from start − 60 min to start + 120 min, both ends included.
    Time
    Resolution one minute. Time points are std::chrono::system_clock::time_point values; the CLI shows local time (UTC+7). The service never calls system_clock::now() itself: it asks an injected Clock.
    Domain names
    Namespace clubhub: Club, Event, Student, Registration, ClubStatus (Pending, Approved, Rejected), RegistrationStatus (Registered, Cancelled, CheckedIn, Waitlisted), RegistrationService with registerStudent, cancel, checkIn, and the EventRepository / RegistrationRepository interfaces with CSV implementations.
    Personal data
    Student id, name, email, optional phone, memberships, attendance. Test data, issues and reports use invented ids (e20230001…) and names only, never real students.
    Rhythm and roles
    Sprint 2 runs in weeks 11 and 12; this lab is week 11. The Product Owner approves the test plan and runs the acceptance test with the client (the instructor or TA); the Sprint 2 Scrum Master keeps the board and the time boxes; one developer is chosen as test lead at sprint planning.
  4. Deliverable folder. Create lab09/ with these files (empty for now); every task fills some of them. Code goes to the usual project folders.
    clubhub/
    ├── include/clubhub/clock.hpp              # Task 3 (Clock, SystemClock)
    ├── src/  (RegistrationService takes a Clock)
    ├── tests/
    │   ├── fake_clock.hpp                     # Task 3
    │   ├── in_memory_repositories.hpp         # Task 3
    │   └── registration_service_test.cpp      # Task 3 (+ Task 4 extra tests)
    ├── .clang-tidy                            # Task 5
    ├── .github/
    │   ├── workflows/ci.yml                   # Task 4 adds the coverage job
    │   ├── pull_request_template.md           # Task 5 (review checklist)
    │   └── ISSUE_TEMPLATE/bug_report.md       # Task 5
    └── lab09/
        ├── test-plan.md                       # Task 1
        ├── test-design.md                     # Task 2 (partitions, boundaries, coverage table)
        ├── test-cases.csv                     # Task 2 (+ Task 4 rows)
        ├── traceability-matrix.csv            # Task 2 (v2 of Lab-05's matrix)
        ├── coverage-summary.txt               # Task 4
        ├── coverage-notes.md                  # Task 4
        ├── code-review.md                     # Task 5
        ├── static-analysis.md                 # Task 5
        ├── bug-reports.md                     # Task 5
        ├── README.md                          # Task 5 (record what changed)
        ├── exploratory-session.md             # Challenge
        └── acceptance-test-script.md          # Challenge
  5. Fallback interfaces. If your Lab-07 code has no service yet, start from this header (adapt names to your code if they already exist; the tests of this lab assume these member functions).
    // include/clubhub/registration_service.hpp (fallback)
    #pragma once
    #include <optional>
    #include <stdexcept>
    #include <string>
    #include "clubhub/clock.hpp"
    #include "clubhub/event.hpp"
    #include "clubhub/registration.hpp"
    
    namespace clubhub {
    
    enum class Rule {
        EventNotFound, EventInPast, AlreadyRegistered, EventFull,
        NotRegistered, CancellationClosed, CheckInClosed
    };
    
    class RuleViolation : public std::runtime_error {
    public:
        explicit RuleViolation(Rule rule)
            : std::runtime_error{"business rule violated"}, rule_{rule} {}
        [[nodiscard]] Rule rule() const noexcept { return rule_; }
    private:
        Rule rule_;
    };
    
    class EventRepository {
    public:
        virtual ~EventRepository() = default;
        // throws RuleViolation{Rule::EventNotFound} for an unknown id
        virtual const Event& get(int eventId) const = 0;
        virtual void save(const Event& event) = 0;
    };
    
    class RegistrationRepository {
    public:
        virtual ~RegistrationRepository() = default;
        virtual std::optional<Registration> find(const std::string& studentId,
                                                 int eventId) const = 0;
        // Registered + CheckedIn registrations of the event
        virtual int countActive(int eventId) const = 0;
        virtual void save(const Registration& registration) = 0;
        virtual void setStatus(const std::string& studentId, int eventId,
                               RegistrationStatus status) = 0;
    };
    
    class RegistrationService {
    public:
        RegistrationService(EventRepository& events, RegistrationRepository& regs,
                            const Clock& clock)
            : events_{events}, regs_{regs}, clock_{clock} {}
        Registration registerStudent(const std::string& studentId, int eventId);
        void cancel(const std::string& studentId, int eventId);
        void checkIn(const std::string& studentId, int eventId);
    private:
        EventRepository& events_;
        RegistrationRepository& regs_;
        const Clock& clock_;
    };
    
    }  // namespace clubhub
1

Test plan for Sprint 2

Easy 45 min · led by the test lead with the Product Owner

Goal

Write a one-page lab09/test-plan.md that says what Sprint 2 will test, at which levels and with which techniques, in which environment, who does what, and which measurable entry and exit criteria decide whether the increment can be released.

Steps

  1. At sprint planning, choose the test lead (one developer). The test lead creates lab09/test-plan.md from the starter with version v1.0, the date, and the names of the Product Owner and test lead.
  2. Section 1 Scope: list the Sprint 2 stories from the board with their FR ids (at least register, cancel and check-in). List at least three items out of scope with a reason (for example reminders: story not in Sprint 2; GUI: optional, not built; monthly report: Sprint 3).
  3. Section 2 Levels and techniques: one row per level (unit, integration, system, acceptance) naming what is tested, the technique (EP and BVA, branch coverage, scripted CLI test cases, Gherkin acceptance), the tool (GoogleTest + CTest, gcovr, a shell script, manual) and who runs it.
  4. Section 3 Environment: compilers and operating systems (GCC 13 and Clang 17 on ubuntu-latest in CI, the compilers on members' laptops), test data (tests/data/*.csv with invented ids only), time control (FakeClock in unit tests; a CLI --now option if you add one).
  5. Sections 4 and 5 Entry and exit criteria: at least three of each, all measurable. The exit criteria must include "branch coverage of RegistrationService ≥ 80 %", "every Must test case passes in CI" and a rule about open defects by severity.
  6. Sections 6 and 7 Roles, schedule and risks: a day-by-day table for weeks 11 and 12, and at least four risks with the test response (for example dates and time zones, corrupt CSV lines, a misunderstood rule, two members editing the same CSV file).
  7. Open a pull request with only this file; the Product Owner reviews and approves it (the approval in GitHub is the sign-off). Commit message: Lab-09 task 1: test plan.

Starter template

# Test plan · ITC Club Hub · Sprint 2 (v1.0, 2026-11-__)
Product Owner: ____   Test lead: ____   Scrum Master (Sprint 2): ____

## 1. Scope
| Story | FR ids | In scope for testing |
|-------|--------|----------------------|
| US-__ Register for an event | FR-11 | yes |
Out of scope (and why):
-

## 2. Test levels and techniques
| Level | What is tested | Technique | Tool | Who |
|-------|----------------|-----------|------|-----|
| Unit | RegistrationService rules | EP, BVA, branch coverage | GoogleTest, gcovr | developers |

## 3. Environment
## 4. Entry criteria
## 5. Exit criteria
## 6. Roles and schedule
| Day | Activity | Who |
|-----|----------|-----|
## 7. Risks
| Risk | Likelihood | Impact | Test response |
|------|------------|--------|---------------|
## 8. Deliverables
test-cases.csv, automated tests in tests/, coverage report (CI artifact),
test summary report at the sprint review

Expected output (excerpt of sections 2 and 5)

| Level       | What is tested                      | Technique              | Tool                | Who        |
| Unit        | RegistrationService, Event          | EP, BVA, branch cov.   | GoogleTest, gcovr   | developers |
| Integration | service + CsvRegistrationRepository | round trip, bad lines  | GoogleTest, tmp dir | developers |
| System      | CLI commands on prepared CSV data   | scripted test cases    | tests/system.sh     | test lead  |
| Acceptance  | Gherkin scenarios of Lab-05         | acceptance test script | manual, with client | PO         |

5. Exit criteria
- every Must test case in test-cases.csv passes in CI (GCC and Clang)
- branch coverage of src/registration_service.cpp >= 80 % (gcovr gate)
- no open Critical or Major defect; Minor ones listed in the test report
- test summary report presented at the sprint review

Show hints

  • One page is enough. A test plan nobody reads is worse than a short one the whole team knows.
  • "Test thoroughly" and "good coverage" are not criteria. Ask: could a teaching assistant check it in one minute without asking you?
  • Entry criteria protect testers from wasting time: "the build is green on main", "the story's acceptance criteria are written", "test data files exist".
  • Risk-based testing: the rules with dates (cancel deadline, check-in window, events in the past) deserve more test cases than listing events.

Acceptance checklist

2

Black-box test design: partitions, boundaries, test cases

Medium 75 min · all developers, one rule each

Goal

Derive equivalence classes and boundary values for the register, create-event, cancel and check-in rules of the brief, turn them into at least 18 test cases with concrete expected results in lab09/test-cases.csv, and trace every case to an FR id.

Steps

  1. Split the rules among the developers (register and create event, cancel, check-in). In lab09/test-design.md, list for each rule its conditions: numeric inputs (seats taken, capacity), times relative to the start, the student's registration status, whether the event exists.
  2. For each condition, write the valid and invalid partitions in a table and give them ids (EP-R1… for register, EP-C1… for cancel, EP-K1… for check-in). Do not forget non-numeric partitions such as "previously Cancelled".
  3. For every ordered partition, write the boundary values (BV-R1…) with a one-minute and one-seat resolution: at least 2-value BVA everywhere, 3-value BVA for the 24-hour deadline and both ends of the check-in window.
  4. Derive the test cases in lab09/test-cases.csv (columns of the starter): every partition and every boundary value covered at least once; at least 18 rows (register and create event ≥ 8, cancel ≥ 5, check-in ≥ 5). The expected result is concrete: the new status, the Rule of the exception, the seat count.
  5. Fill level (unit, integration, system, acceptance) and leave automated_by empty for now; Task 3 fills it with the GoogleTest name. System-level rows describe CLI commands exactly.
  6. Trace: map every Gherkin scenario of Lab-05 for these stories to at least one test case. Copy lab05/traceability-matrix.csv to lab09/traceability-matrix.csv, raise its version to v2 and fill the test_cases column; any FR id of Sprint 2 without a test case goes into a Gaps list with a decision.
  7. Another developer checks the coverage table at the end of test-design.md (partition or boundary id → test case ids) and signs it with their name. Commit: Lab-09 task 2: test design.

Starter

# Test design · Sprint 2 (v1.0)

## FR-11 registerStudent (capacity C = 3 in the unit tests)
| Condition | Valid partitions | Invalid partitions | Boundary values |
|-----------|------------------|--------------------|-----------------|
| seats taken k | EP-R1 0 ≤ k ≤ C−1 | EP-R2 k ≥ C (EventFull) | BV-R1 k = C−1, BV-R2 k = C; C = 1: k = 0, 1 |
| time t vs start S | EP-R3 t < S | EP-R4 t ≥ S (EventInPast) | BV-R3 S − 1 min, BV-R4 S |
| student's status | EP-R5 none, EP-R6 Cancelled | EP-R7 Registered or CheckedIn | n/a |
| event id | EP-R8 exists | EP-R9 unknown (EventNotFound) | n/a |

## FR-12 cancel · ## FR-13 checkIn · ## FR-05 create event   (TODO)

## Coverage table (checked by: ____)
| Partition / boundary | Test cases |
|----------------------|------------|
| EP-R1 | TC-REG-01 |
id,fr_id,rule,technique,partition_or_boundary,precondition,steps,input,expected,level,automated_by
TC-REG-01,FR-11,register,EP,EP-R1 seat free,"C=3, k=1, now=S-72h",registerStudent,"e20230001, event 1","Registered; countActive = 2",unit,
TC-REG-02,FR-11,register,BVA,BV-R1 last seat,"C=3, k=2, now=S-72h",registerStudent,"e20230003, event 1","Registered; event full",unit,
TC-REG-03,FR-11,register,BVA,BV-R2 no seat left,"C=3, k=3, now=S-72h",registerStudent,"e20230004, event 1","RuleViolation EventFull; countActive = 3",unit,
TC-CAN-02,FR-12,cancel,BVA,BV-C2 on the deadline,"e20230001 Registered, now=S-24h",cancel,"e20230001, event 1","Cancelled; countActive - 1",unit,
TC-SYS-01,FR-12,cancel,BVA,BV-C3 just after deadline,"data/ with event 1 at 2026-11-20 18:00","clubhub --now ""2026-11-19 18:01"" cancel e20230001 1","e20230001, event 1","prints Error: cancellation closed; exit code 2",system,
# TODO: at least 18 rows in total

Expected output (summary at the top of test-design.md)

RulePartitionsBoundary valuesTest casesLevelsGaps
FR-11 register96108 unit, 2 systemnone
FR-05 create event323unitclub approval: Sprint 3
FR-12 cancel5465 unit, 1 systemnone
FR-13 check-in6676 unit, 1 acceptancewindow confirmed by PO on 2026-11-17
Total231826

Show hints

  • Use a small capacity (C = 3) in unit tests: the boundary is the same idea as C = 30 but needs fewer setup calls.
  • The 24-hour rule belongs to cancel only. A test "register at exactly S − 24 h → Registered" protects against a developer applying it to registration too.
  • "Check in twice" and "check in after cancelling" are separate invalid partitions of the student's status; the time window is a separate condition with its own boundaries.
  • One test case may cover one valid partition of several conditions at once, but each invalid partition needs its own test case, otherwise you cannot tell which rule rejected the request.
  • In CSV, quote fields that contain commas, and double the quotes inside a quoted field (""2026-11-19 18:01"").

Acceptance checklist

3

GoogleTest unit tests with fixtures, boundaries and an injected clock

Medium 100 min · every developer writes tests

Goal

Implement every unit-level row of test-cases.csv as a GoogleTest test: a fixture with in-memory repositories and a FakeClock, parameterised tests for the boundaries, and assertions on the exact rule that was violated. All tests pass locally and in CI on GCC and Clang.

Steps

  1. Add include/clubhub/clock.hpp with Clock and SystemClock (as on slide 15 of the chapter). Change the RegistrationService constructor to take a const Clock&; main.cpp passes a SystemClock. Then git grep "system_clock::now" src/ must find nothing.
  2. Add tests/fake_clock.hpp and tests/in_memory_repositories.hpp (starter). Unit tests must never read or write the real data/ folder.
  3. In tests/registration_service_test.cpp, create the fixture RegistrationServiceTest and implement each unit-level test case as a TEST_F with a // TC-REG-03 comment above it. Write the test name into the automated_by column of test-cases.csv.
  4. Implement the boundaries as parameterised tests: CancelDeadlineTest (D − 1 min, D, D + 1 min, S), CapacityBoundaryTest (C = 1 and C = 3, k = C − 1 and k = C) and CheckInWindowTest (S − 61, S − 60, S + 120, S + 121 min).
  5. Assert the rule, not only the exception type: use the expectRule helper from the hints, so that an EventFull thrown where EventInPast was expected fails the test.
  6. Add the new test files to clubhub_tests in CMakeLists.txt, then run cmake --build build && ctest --test-dir build --output-on-failure. A test that fails because the code is wrong is good news: fix the code in a separate commit that names the test case.
  7. Push, open a pull request, and make CI green in both matrix legs. Check that every developer authored test commits: git shortlog -sn -- tests/.

Starter code

// tests/in_memory_repositories.hpp
#pragma once
#include <algorithm>
#include <map>
#include <utility>
#include "clubhub/registration_service.hpp"

namespace clubhub {

class InMemoryEventRepository final : public EventRepository {
public:
    const Event& get(int eventId) const override {
        const auto it = events_.find(eventId);
        if (it == events_.end()) {
            throw RuleViolation{Rule::EventNotFound};
        }
        return it->second;
    }
    void save(const Event& event) override { events_.insert_or_assign(event.id(), event); }
private:
    std::map<int, Event> events_;
};

class InMemoryRegistrationRepository final : public RegistrationRepository {
public:
    std::optional<Registration> find(const std::string& studentId,
                                     int eventId) const override {
        const auto it = regs_.find({studentId, eventId});
        if (it == regs_.end()) { return std::nullopt; }
        return it->second;
    }
    int countActive(int eventId) const override {
        return static_cast<int>(std::ranges::count_if(regs_, [eventId](const auto& kv) {
            const Registration& r = kv.second;
            return r.eventId == eventId && (r.status == RegistrationStatus::Registered ||
                                           r.status == RegistrationStatus::CheckedIn);
        }));
    }
    void save(const Registration& r) override { regs_.insert_or_assign({r.studentId, r.eventId}, r); }
    void setStatus(const std::string& studentId, int eventId,
                   RegistrationStatus status) override {
        regs_.at({studentId, eventId}).status = status;
    }
private:
    std::map<std::pair<std::string, int>, Registration> regs_;
};

}  // namespace clubhub
// tests/registration_service_test.cpp
#include <gtest/gtest.h>
#include <chrono>
#include <ostream>
#include <string>
#include "fake_clock.hpp"
#include "in_memory_repositories.hpp"

using namespace std::chrono;
using namespace clubhub;

class RegistrationServiceTest : public ::testing::Test {
protected:
    static constexpr int kEventId = 1;
    // 2026-11-20 18:00 local time (UTC+7) = 11:00 UTC
    const TimePoint start = sys_days{2026y / November / 20} + 11h;
    FakeClock clock{start - 72h};
    InMemoryEventRepository events;
    InMemoryRegistrationRepository regs;
    RegistrationService service{events, regs, clock};

    void SetUp() override {
        events.save(Event{kEventId, "C++ Workshop", start, "B-201", 3});
    }
    // registers students s1..sk so that k seats are taken
    void fillSeats(int k) {
        for (int i = 1; i <= k; ++i) {
            service.registerStudent("s" + std::to_string(i), kEventId);
        }
    }
};

// TC-REG-01 (FR-11, EP-R1 seat free)
TEST_F(RegistrationServiceTest, RegistersWhenSeatIsFree) {
    fillSeats(1);
    service.registerStudent("e20230001", kEventId);
    const auto reg = regs.find("e20230001", kEventId);
    ASSERT_TRUE(reg.has_value());
    EXPECT_EQ(reg->status, RegistrationStatus::Registered);
    EXPECT_EQ(regs.countActive(kEventId), 2);
}

// TC-REG-03 (FR-11, BV-R2 no seat left)
TEST_F(RegistrationServiceTest, RejectsWhenFull) {
    // TODO: fill 3 seats, expect Rule::EventFull, count stays 3
}

// TC-CAN-01..04 (FR-12, 3-value BVA around D = S - 24 h, plus S)
struct CancelCase {
    minutes beforeStart;
    bool allowed;
};
// readable parameter values in gtest and ctest output
void PrintTo(const CancelCase& c, std::ostream* os) {
    *os << c.beforeStart.count() << "min_before_" << (c.allowed ? "allowed" : "rejected");
}
class CancelDeadlineTest : public RegistrationServiceTest,
                           public ::testing::WithParamInterface<CancelCase> {};

TEST_P(CancelDeadlineTest, AppliesThe24HourRule) {
    // TODO: register e20230001, clock.set(start - beforeStart), cancel,
    //       check the status (allowed) or Rule::CancellationClosed
}
INSTANTIATE_TEST_SUITE_P(Boundaries, CancelDeadlineTest,
    ::testing::Values(CancelCase{24h + 1min, true} /* TODO: D, D + 1 min, S */));

// TODO: CapacityBoundaryTest, CheckInWindowTest, remaining TEST_F cases

Expected output

$ ctest --test-dir build --output-on-failure
Test project /home/runner/work/clubhub/clubhub/build
      Start  1: EventTest.KeepsTitleAndCapacity
 1/34 Test  #1: EventTest.KeepsTitleAndCapacity .................................   Passed    0.01 sec
      Start  2: RegistrationServiceTest.RegistersWhenSeatIsFree
 2/34 Test  #2: RegistrationServiceTest.RegistersWhenSeatIsFree .................   Passed    0.01 sec
...
      Start 21: Boundaries/CancelDeadlineTest.AppliesThe24HourRule/1439min_before_rejected
21/34 Test #21: Boundaries/CancelDeadlineTest.AppliesThe24HourRule/1439min_before_rejected ...   Passed    0.01 sec
...
34/34 Test #34: Window/CheckInWindowTest.AppliesTheWindow/121min_after_start_rejected ........   Passed    0.01 sec

100% tests passed, 0 tests failed out of 34

Total Test time (real) =   0.52 sec

Show hints

  • A helper that returns the violated rule, so tests can compare it:
    template <typename F>
    std::optional<Rule> violatedRule(F&& action) {
        try { action(); } catch (const RuleViolation& e) { return e.rule(); }
        return std::nullopt;
    }
    // usage: EXPECT_EQ(violatedRule([&] { service.cancel("e20230001", kEventId); }),
    //                  Rule::CancellationClosed);
  • Fixture members are created fresh for every test, so tests cannot influence each other. Never use static state in the fakes.
  • A parameterised test that inherits the fixture needs the fixture's members to be protected, not private.
  • Without a PrintTo function for your parameter struct, CTest names the cases …/8-byte object <A1-05 …>. Give every parameter struct a PrintTo (see the starter) so a red test names its boundary.
  • GoogleTest discourages underscores in test and suite names; keep the TC- id in the comment and in the CSV instead of in the name.
  • If a test needs sleep or the real date to pass, the design is wrong: move the time into the FakeClock.

Acceptance checklist

4

Statement and branch coverage with gcov and gcovr in CI

Hard 75 min · led by the developer who owns the pipeline

Goal

Add a coverage job to the pipeline that measures statement and branch coverage with gcov and gcovr, fails when branch coverage of RegistrationService drops below 80 %, publishes the HTML report, and document which lines stay uncovered and why.

Steps

  1. Add the CLUBHUB_COVERAGE option of the starter to CMakeLists.txt, before the first add_library or add_executable, so that every target is instrumented.
  2. On Linux, macOS or WSL with GCC: configure build-cov with -DCMAKE_BUILD_TYPE=Debug -DCLUBHUB_COVERAGE=ON, build, run ctest, then run gcovr with --html-details coverage/index.html --print-summary and open the report. (Windows only: skip to step 3 and read the report from the CI artifact.)
  3. Add the coverage job of the starter to .github/workflows/ci.yml. It runs after your build job (adjust needs: to your job id), installs gcovr, uploads coverage/ as the artifact coverage-report and runs the gate --fail-under-branch 80 on src/registration_service.cpp.
  4. Open the report for registration_service.cpp. For every red line and every yellow (partially taken) branch, decide: missing test, or code that cannot be reached? Add tests for the missing ones, each as a new row in test-cases.csv with technique branch, until branch coverage is at least 80 %.
  5. Save the --print-summary output for the whole src/ and for registration_service.cpp into lab09/coverage-summary.txt, with the commit hash.
  6. Write lab09/coverage-notes.md: a table of every line or branch that stays uncovered, why (Should feature not built, defensive code, file-system failure that needs fault injection) and the decision. You may mark at most three truly unreachable lines with // GCOVR_EXCL_LINE plus a comment explaining why.
  7. Commit: Lab-09 task 4: coverage in CI. The pull request must show the coverage job green.

Starter

# CMakeLists.txt, near the top (before add_library / add_executable)
option(CLUBHUB_COVERAGE "Instrument the build for gcov" OFF)
if(CLUBHUB_COVERAGE)
  if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    add_compile_options(--coverage -O0 -g)
    add_link_options(--coverage)
  else()
    message(WARNING "Coverage needs GCC or Clang; option ignored")
  endif()
endif()
# .github/workflows/ci.yml: add this job next to your build job
  coverage:
    runs-on: ubuntu-latest
    needs: build            # the id of your Lab-08 build job
    steps:
      - uses: actions/checkout@v4
      - name: Install gcovr
        run: pipx install gcovr
      - name: Configure with coverage (GCC)
        run: cmake -S . -B build-cov -DCMAKE_BUILD_TYPE=Debug -DCLUBHUB_COVERAGE=ON
      - name: Build and test
        run: |
          cmake --build build-cov -j
          ctest --test-dir build-cov --output-on-failure
      - name: Coverage report
        run: |
          mkdir -p coverage
          gcovr -r . build-cov --filter src/ \
                --exclude-throw-branches --exclude-unreachable-branches \
                --html-details coverage/index.html --print-summary
      - name: Branch coverage gate (RegistrationService)
        run: |
          gcovr -r . build-cov --filter 'src/registration_service\.cpp' \
                --exclude-throw-branches --exclude-unreachable-branches \
                --print-summary --fail-under-branch 80
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/

Expected output

flowchart LR
  P["push / pull request"] --> B["build matrix
GCC 13 · Clang 17"] B --> T["ctest
34 tests"] T --> C["coverage job
--coverage -O0"] C --> R["gcovr HTML report
artifact coverage-report"] C --> G{"branches of
registration_service.cpp
at least 80 %?"} G -- yes --> OK(["check green, merge allowed"]) G -- no --> KO(["check red, merge blocked"])
# lab09/coverage-summary.txt (commit 9e41b7a)
src/ (all files)
lines: 91.2% (145 out of 159)
functions: 96.0% (24 out of 25)
branches: 78.6% (66 out of 84)

src/registration_service.cpp
lines: 96.3% (52 out of 54)
functions: 100.0% (7 out of 7)
branches: 86.4% (19 out of 22)
File:lineCodeWhy uncoveredDecision
registration_service.cpp:58return waitlist(studentId, eventId);Waiting list is the Should story FR-14, switched off in Sprint 2Tests come with the story (Sprint 3)
csv_registration_repository.cpp:74throw std::runtime_error{"cannot open"}Needs a file that cannot be openedIntegration test with a read-only directory, Linux only; backlog item #58
registration_service.cpp:12default: of the switch over RuleUnreachable: every enum value has a case// GCOVR_EXCL_LINE with a comment

Show hints

  • Without --exclude-throw-branches, every call that may throw adds hidden branches for the exception path, and branch coverage looks much worse than it is.
  • Coverage from Clang's --coverage needs gcovr --gcov-executable "llvm-cov gcov"; the CI job uses GCC so it does not need this.
  • Old .gcda files from an earlier build distort the numbers. If the report looks odd locally, delete build-cov/ and rebuild.
  • A yellow if with "1/2" usually means you tested only one outcome: look at the condition and write the test for the other outcome, not a test that merely calls the function again.
  • Do not chase 100 %. The goal is to know what is untested and why, which the notes table documents.

Acceptance checklist

5

Quality assurance: peer review, static analysis and bug reports

Hard 100 min · every member reviews; the Scrum Master records

Goal

Run the three quality-assurance practices of the chapter on your own code: a checklist-based peer review of another member's pull request, one static-analysis run whose findings you fix or justify, and three reproducible bug reports filed as GitHub issues, one of them driven to Closed. Finish by recording what changed.

Steps

  1. Add .github/pull_request_template.md with the C++ review checklist and .github/ISSUE_TEMPLATE/bug_report.md (starters below). Merge them first so that the following PRs and issues use them.
  2. Peer review. Rotate so that every member reviews a pull request written by someone else (for example the Task 3 test PRs). The reviewer leaves at least three comments, each starting with its checklist area (Ownership:, const:, Error handling:, Naming:, Tests:); the author answers each and pushes fixes; approve only when CI is green. Summarise in lab09/code-review.md: PR link, author, reviewer, comments per area, defects found, time spent.
  3. Static analysis. Add .clang-tidy, configure with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON, then run clang-tidy -p build src/*.cpp and cppcheck --enable=warning,style,performance --std=c++20 --project=build/compile_commands.json. Record every finding in lab09/static-analysis.md (tool, file:line, check id, decision, commit).
  4. Fix at least five findings (or all, if there are fewer). A finding you do not fix is suppressed with // NOLINT(check-id): reason or a cppcheck inline suppression and listed with its reason. Re-run both tools and paste the final summary. Optional: add a CI step that runs cppcheck with --error-exitcode=1.
  5. Bug reports. File three GitHub issues with the template, label bug, from real observations of this lab (a failing test in Task 3, a review finding, a CLI run). Each has environment, severity, steps to reproduce, expected and actual result, and the FR id; the Product Owner sets the priority. List them in lab09/bug-reports.md.
  6. Drive at least one bug through the life cycle: Assigned, a fixing PR whose description says Fixes #n and that adds a regression test, Verified by the reporter, Closed. Record the states with dates in bug-reports.md.
  7. Record what changed. Create lab09/README.md from the starter: the table of this lab's files with their main authors, the inputs reused from Labs 05 to 08 with their commit or version, the results (test cases, coverage, findings, bugs) and a change-log row. Commit and open the pull request Lab-09 – <team name>.

Starter files

<!-- .github/pull_request_template.md -->
## What and why
Closes #

## Review checklist (reviewer ticks; comments start with the area)
- [ ] Ownership: no naked new/delete; owners are values or unique_ptr (R.11, R.20)
- [ ] const: queries are const member functions; big inputs by const& (Con.2, F.16)
- [ ] Error handling: broken rules throw RuleViolation; no empty catch; RAII (E.2, E.6)
- [ ] Naming: PascalCase types, camelCase functions, member_ suffix (NL.8)
- [ ] Tests: every new or changed rule has a test that fails without the change
- [ ] Readability: short functions, named constants, no dead code (ES.45)
- [ ] CI green: build matrix, tests, coverage gate
---
name: Bug report
about: Report a defect in ITC Club Hub
title: "[Bug] <what happens> (violates FR-xx)"
labels: bug
---
**Environment**: commit <sha> · OS · compiler and version · Debug/Release
**Severity**: Critical | Major | Minor | Trivial
**Priority** (Product Owner): High | Medium | Low
**Requirement**: FR-xx · **Found by**: TC-xxx | review | exploratory session

**Preconditions**

**Steps to reproduce**
1.

**Expected result**

**Actual result**

**Attachments and notes** (console output, CSV excerpt; invented data only)
# .clang-tidy
Checks: >
  bugprone-*, cppcoreguidelines-*, modernize-*, performance-*,
  readability-*, -modernize-use-trailing-return-type,
  -readability-identifier-length
WarningsAsErrors: 'bugprone-*'
# Lab-09 · Testing and quality assurance (v1.0, 2026-11-__)

## Files of this lab
| File | Content | Task | Main author(s) |
|------|---------|------|----------------|
| lab09/test-plan.md | Sprint 2 test plan, approved by the PO | 1 | |
| tests/registration_service_test.cpp | fixture, TEST_P boundaries | 3 | |

## Inputs reused from earlier labs
| Input | Version or commit | Used for |
|-------|-------------------|----------|
| lab05/traceability-matrix.csv | v1, commit ____ | FR ids, copied to lab09 as v2 |
| .github/workflows/ci.yml (Lab-08) | commit ____ | coverage job added |

## Results
- Test cases: __ designed, __ automated, __ manual
- Coverage of RegistrationService: lines __ %, branches __ %
- Static analysis: __ findings, __ fixed, __ suppressed with a reason
- Bugs: #__ (Closed), #__, #__

## Change log
| Date | Version | Change | Author |
|------|---------|--------|--------|

Expected output (excerpt of static-analysis.md)

ToolLocationCheckDecisionCommit
clang-tidyregistration_service.cpp:18performance-unnecessary-value-paramfixed: const std::string&3f0a9c2
clang-tidyregistration_service.hpp:52cppcoreguidelines-avoid-const-or-ref-data-memberssuppressed: the service is never copied or assigned; references express "must not be null"3f0a9c2
cppcheckevent.cpp:12uninitMemberVarfixed: int capacity_{0};77b1e05
cppcheckreport.cpp:27useStlAlgorithmfixed: std::ranges::count_if77b1e05
Final run: clang-tidy 0 warnings (1 suppressed with reason), cppcheck 0 findings.

Show hints

  • Review the code, not the person: "Error handling: this catch (...) hides a failed save; could we let it propagate?" is actionable; "this is bad" is not.
  • clang-tidy on Windows: pass the same compile_commands.json; generating it needs the Ninja or Makefile generator (cmake -G Ninja), not the Visual Studio generator.
  • A realistic bug for the report: the test for "register again after cancelling" fails because the duplicate check looks for any registration instead of an active one.
  • gh issue create --template bug_report.md files an issue from the terminal; the web form works just as well.
  • The README's "Main author(s)" column is checked against git log --format='%an' -- <file>.

Acceptance checklist

Challenge: exploratory testing and an acceptance test with the client

Hard 90–120 min · optional, extra credit

Scenario

Scripted tests only find the defects someone thought of. Before the sprint review, a pair explores the CLI for 45 minutes without a script, guided by a charter, and files what it finds. Then the Product Owner runs an acceptance test script, derived from the Lab-05 Gherkin criteria, together with the client (your instructor or teaching assistant) and records the verdict.

Requirements

  1. Charter. In lab09/exploratory-session.md, write one charter in the form Explore <area> with <resources> to discover <information>, for an area your automated tests do not reach: CLI input handling, dates near midnight, hand-edited CSV files, full events.
  2. Session. One tester drives the CLI, one takes notes; set a 45-minute timer and stop when it rings. Notes carry time stamps and are classified as observation, bug, question or test idea.
  3. Debrief. File every bug as an issue with the template; turn at least two test ideas into new rows of test-cases.csv; record the share of time spent on the charter.
  4. Acceptance test script. In lab09/acceptance-test-script.md, turn every Gherkin scenario of the register, cancel and check-in stories into numbered steps with the exact CLI command, prepared data or --now time, the expected output and a Pass / Fail column.
  5. Run it with the client in a 20-minute slot led by the Product Owner. Record the result of every step, the client's comments and the sign-off (name, date). New wishes become backlog items, not silent fixes.

Skeleton

# Exploratory session · ITC Club Hub CLI
Charter: Explore ________ with ________ to discover ________
Tester: ____ (driver)  Note-taker: ____   Date: ____  Commit: ____
Time box: 45 min, started __:__

| Time | Type | Note |
|------|------|------|
| 14:03 | bug | ... |

Debrief: on-charter __ %, bugs #__, questions __, new test cases TC-__

# Acceptance test script · Sprint 2 (v1.0)
Data: tests/data/acceptance/ copied to data/ before step 1
| # | Scenario (Lab-05) | Command | Expected | Pass/Fail | Client comment |
|---|-------------------|---------|----------|-----------|----------------|
| 1 | Register when seats are free | clubhub --now "2026-11-17 10:00" register e20230001 1 | Registered. | | |
Sign-off: ______________ (client)   ______________ (Product Owner)   Date: ____

Expected output

Charter: Explore registration and cancellation in the CLI with unusual student ids,
times near midnight and full events, to discover input-handling and deadline defects.
Tester: Dara (driver), Sokha (notes) · 2026-11-19 14:00-14:45 · commit 7c1d2e0

14:03 bug        register " e20230001" (leading space) is accepted as a new student;
                 duplicate check bypassed -> #61 (Major)
14:11 observ.    event at 00:30, cancel at 00:29 the day before -> rejected, correct
14:20 question   two terminals register the last seat at once: both succeed?
                 CSV is rewritten by each process -> ask team, backlog #64
14:34 bug        capacity "30abc" accepted as 30 by event add -> #62 (Minor)
Debrief: 80 % on charter, 2 bugs, 1 question, 2 new test cases (TC-REG-11, TC-EVT-04)

Acceptance run 2026-11-24 with the TA: 9 steps, 8 Pass, 1 Fail (step 7: attendance list
sorted by id, client expects by name -> backlog item, not a defect). Signed: both.

Show hints

  • The charter is a mission, not a script: it tells you where to look and what kind of information to bring back, and leaves the how to the tester.
  • Useful heuristics: boundaries (0, 1, many, too many), wrong types ("abc" for a number), empty and very long input, leading and trailing spaces, times at midnight and at the deadline.
  • Prepare the acceptance data files in advance and copy them before every run, so the client sees the same state each time.
  • A failed acceptance step can be a defect (the product does not do what was agreed) or a new need (the agreement was incomplete). Classify it with the client before you leave the room.

Acceptance checklist

Grading rubric

ComponentPointsFull marks when…
Task 1 · Test plan for Sprint 210One-page plan with scope, four levels with techniques and tools, environment, measurable entry and exit criteria, schedule, risks; approved by the Product Owner in a PR.
Task 2 · Black-box test design20Partitions (valid and invalid) and boundary values with ids for all four rules; at least 18 traceable test cases with concrete expected results; checked coverage table; traceability matrix v2 with decided gaps.
Task 3 · GoogleTest unit tests25Injected clock; fixture with in-memory repositories; every unit test case automated and named in the CSV; three parameterised boundary suites; exact rule assertions; green in CI on GCC and Clang; every developer contributed tests.
Task 4 · Coverage in CI15Coverage option, CI job with artifact and a working 80 % branch gate on the service; summary with commit; every remaining uncovered line explained.
Task 5 · Review, static analysis, bug reports20Checklist-based reviews by every member; all findings fixed or justified; three reproducible bug reports, one closed through a tested fix; complete README.
Code and document quality10Readable tests (Arrange, Act, Assert; one behaviour each), consistent ids across CSV, tests and issues, invented test data only, one commit per step with clear messages.
★ Challenge (extra credit)+20Chartered 45-minute exploratory session with classified notes and filed bugs; acceptance test script covering all scenarios, run and signed with the client.
Total100 (+20)
Automatic deductions: -5 per test that asserts nothing (max -15) · -10 if the CI pipeline (build matrix, tests and coverage gate) is not green on main at the deadline · -5 if any RegistrationService test depends on the real clock (system_clock::now(), the current date or sleep) · -5 per test case in test-cases.csv without an FR id or a concrete expected result (max -10) · -5 if a coverage percentage is reported without the list of uncovered lines and reasons · -5 per bug report without steps to reproduce or without expected and actual result (max -10) · -5 if real personal data of students appears in test data, issues or reports · -5 if lab09/README.md does not list every Lab-09 file or has no change-log row.

Submission

  1. Repository layout (new or changed in this lab):
    clubhub/
    ├── CMakeLists.txt                     # CLUBHUB_COVERAGE option, new test files
    ├── .clang-tidy
    ├── .github/
    │   ├── workflows/ci.yml               # + coverage job
    │   ├── pull_request_template.md
    │   └── ISSUE_TEMPLATE/bug_report.md
    ├── include/clubhub/clock.hpp
    ├── src/                               # RegistrationService uses the Clock
    ├── tests/
    │   ├── fake_clock.hpp
    │   ├── in_memory_repositories.hpp
    │   └── registration_service_test.cpp
    └── lab09/
        ├── test-plan.md  test-design.md  test-cases.csv  traceability-matrix.csv
        ├── coverage-summary.txt  coverage-notes.md
        ├── code-review.md  static-analysis.md  bug-reports.md
        ├── README.md
        └── exploratory-session.md  acceptance-test-script.md    # challenge
  2. Self-check from a clean checkout; all commands must succeed (the coverage part on Linux, macOS or WSL with GCC):
    cmake -S . -B build && cmake --build build && ctest --test-dir build
    cmake -S . -B build-cov -DCMAKE_BUILD_TYPE=Debug -DCLUBHUB_COVERAGE=ON
    cmake --build build-cov && ctest --test-dir build-cov
    gcovr -r . build-cov --filter 'src/registration_service\.cpp' \
          --exclude-throw-branches --exclude-unreachable-branches --fail-under-branch 80
    git grep -n "system_clock::now" src/ | grep -v clock.hpp    # must print nothing
    python lab09/check_test_cases.py                             # the script below
    # lab09/check_test_cases.py: every test case has an FR id and an expected result
    import csv, sys
    rows = list(csv.DictReader(open("lab09/test-cases.csv", encoding="utf-8")))
    ids = [r["id"] for r in rows]
    missing = [r["id"] for r in rows if not r["fr_id"].strip() or not r["expected"].strip()]
    duplicates = sorted({i for i in ids if ids.count(i) > 1})
    unit_not_automated = [r["id"] for r in rows if r["level"] == "unit" and not r["automated_by"].strip()]
    print(f"{len(rows)} test cases; missing FR/expected: {missing}; duplicates: {duplicates}; "
          f"unit rows without a test: {unit_not_automated}")
    sys.exit(1 if missing or duplicates or unit_not_automated or len(rows) < 18 else 0)
  3. Work on branch lab-09 and open the pull request Lab-09 – <team name> against main, with the CI checks green and one approving review. Link the three bug issues and the coverage artifact in the PR description.