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.
tests/ and in lab09/, so that individual contributions are visible in the Git history.Setup
25 min- 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 - 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.Input File What you need from it Fallback if missing Acceptance criteria lab05/user-stories.mdGherkin scenarios of the register, cancel and check-in stories Write one Given/When/Then per rule of the case-study brief below into lab09/test-design.md.Traceability matrix lab05/traceability-matrix.csvFR ids, story ids, an empty test column Use 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 machine lab06/(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 done lab07/and the GitHub Project boardWhich stories are in Sprint 2; the team's DoD Stories: register, cancel, check-in, attendance list. DoD: reviewed PR, tests pass in CI, acceptance criteria met. C++ code include/clubhub/,src/,tests/RegistrationServicewithregisterStudent,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; protectedmainThe coveragejob of Task 4 also works on its own; protectmainwith one required review. - 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 mintostart + 120 min, both ends included. - Time
- Resolution one minute. Time points are
std::chrono::system_clock::time_pointvalues; the CLI shows local time (UTC+7). The service never callssystem_clock::now()itself: it asks an injectedClock. - Domain names
- Namespace
clubhub:Club,Event,Student,Registration,ClubStatus(Pending,Approved,Rejected),RegistrationStatus(Registered,Cancelled,CheckedIn,Waitlisted),RegistrationServicewithregisterStudent,cancel,checkIn, and theEventRepository/RegistrationRepositoryinterfaces 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.
- 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 - 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
Test plan for Sprint 2
Easy 45 min · led by the test lead with the Product OwnerGoal
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
- At sprint planning, choose the test lead (one developer). The test lead creates
lab09/test-plan.mdfrom the starter with versionv1.0, the date, and the names of the Product Owner and test lead. - 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).
- 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.
- Section 3 Environment: compilers and operating systems (GCC 13 and Clang 17 on
ubuntu-latestin CI, the compilers on members' laptops), test data (tests/data/*.csvwith invented ids only), time control (FakeClockin unit tests; a CLI--nowoption if you add one). - 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. - 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).
- 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
- 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
Black-box test design: partitions, boundaries, test cases
Medium 75 min · all developers, one rule eachGoal
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
- 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. - 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". - 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. - 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, theRuleof the exception, the seat count. - Fill
level(unit, integration, system, acceptance) and leaveautomated_byempty for now; Task 3 fills it with the GoogleTest name. System-level rows describe CLI commands exactly. - Trace: map every Gherkin scenario of Lab-05 for these stories to at least one test case. Copy
lab05/traceability-matrix.csvtolab09/traceability-matrix.csv, raise its version to v2 and fill thetest_casescolumn; any FR id of Sprint 2 without a test case goes into a Gaps list with a decision. - 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)
| Rule | Partitions | Boundary values | Test cases | Levels | Gaps |
|---|---|---|---|---|---|
| FR-11 register | 9 | 6 | 10 | 8 unit, 2 system | none |
| FR-05 create event | 3 | 2 | 3 | unit | club approval: Sprint 3 |
| FR-12 cancel | 5 | 4 | 6 | 5 unit, 1 system | none |
| FR-13 check-in | 6 | 6 | 7 | 6 unit, 1 acceptance | window confirmed by PO on 2026-11-17 |
| Total | 23 | 18 | 26 |
- 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
GoogleTest unit tests with fixtures, boundaries and an injected clock
Medium 100 min · every developer writes testsGoal
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
- Add
include/clubhub/clock.hppwithClockandSystemClock(as on slide 15 of the chapter). Change theRegistrationServiceconstructor to take aconst Clock&;main.cpppasses aSystemClock. Thengit grep "system_clock::now" src/must find nothing. - Add
tests/fake_clock.hppandtests/in_memory_repositories.hpp(starter). Unit tests must never read or write the realdata/folder. - In
tests/registration_service_test.cpp, create the fixtureRegistrationServiceTestand implement each unit-level test case as aTEST_Fwith a// TC-REG-03comment above it. Write the test name into theautomated_bycolumn oftest-cases.csv. - 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) andCheckInWindowTest(S − 61, S − 60, S + 120, S + 121 min). - Assert the rule, not only the exception type: use the
expectRulehelper from the hints, so that an EventFull thrown where EventInPast was expected fails the test. - Add the new test files to
clubhub_testsinCMakeLists.txt, then runcmake --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. - 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
- 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
staticstate in the fakes. - A parameterised test that inherits the fixture needs the fixture's members to be
protected, notprivate. - Without a
PrintTofunction for your parameter struct, CTest names the cases…/8-byte object <A1-05 …>. Give every parameter struct aPrintTo(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
sleepor the real date to pass, the design is wrong: move the time into theFakeClock.
Acceptance checklist
Statement and branch coverage with gcov and gcovr in CI
Hard 75 min · led by the developer who owns the pipelineGoal
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
- Add the
CLUBHUB_COVERAGEoption of the starter toCMakeLists.txt, before the firstadd_libraryoradd_executable, so that every target is instrumented. - On Linux, macOS or WSL with GCC: configure
build-covwith-DCMAKE_BUILD_TYPE=Debug -DCLUBHUB_COVERAGE=ON, build, runctest, then run gcovr with--html-details coverage/index.html --print-summaryand open the report. (Windows only: skip to step 3 and read the report from the CI artifact.) - Add the
coveragejob of the starter to.github/workflows/ci.yml. It runs after your build job (adjustneeds:to your job id), installs gcovr, uploadscoverage/as the artifactcoverage-reportand runs the gate--fail-under-branch 80onsrc/registration_service.cpp. - 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 intest-cases.csvwith techniquebranch, until branch coverage is at least 80 %. - Save the
--print-summaryoutput for the wholesrc/and forregistration_service.cppintolab09/coverage-summary.txt, with the commit hash. - 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_LINEplus a comment explaining why. - 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:line | Code | Why uncovered | Decision |
|---|---|---|---|
registration_service.cpp:58 | return waitlist(studentId, eventId); | Waiting list is the Should story FR-14, switched off in Sprint 2 | Tests come with the story (Sprint 3) |
csv_registration_repository.cpp:74 | throw std::runtime_error{"cannot open"} | Needs a file that cannot be opened | Integration test with a read-only directory, Linux only; backlog item #58 |
registration_service.cpp:12 | default: of the switch over Rule | Unreachable: every enum value has a case | // GCOVR_EXCL_LINE with a comment |
- 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
--coverageneedsgcovr --gcov-executable "llvm-cov gcov"; the CI job uses GCC so it does not need this. - Old
.gcdafiles from an earlier build distort the numbers. If the report looks odd locally, deletebuild-cov/and rebuild. - A yellow
ifwith "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
Quality assurance: peer review, static analysis and bug reports
Hard 100 min · every member reviews; the Scrum Master recordsGoal
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
- Add
.github/pull_request_template.mdwith 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. - 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. - Static analysis. Add
.clang-tidy, configure with-DCMAKE_EXPORT_COMPILE_COMMANDS=ON, then runclang-tidy -p build src/*.cppandcppcheck --enable=warning,style,performance --std=c++20 --project=build/compile_commands.json. Record every finding inlab09/static-analysis.md(tool, file:line, check id, decision, commit). - Fix at least five findings (or all, if there are fewer). A finding you do not fix is suppressed with
// NOLINT(check-id): reasonor 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. - 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 inlab09/bug-reports.md. - Drive at least one bug through the life cycle: Assigned, a fixing PR whose description says
Fixes #nand that adds a regression test, Verified by the reporter, Closed. Record the states with dates inbug-reports.md. - Record what changed. Create
lab09/README.mdfrom 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 requestLab-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)
| Tool | Location | Check | Decision | Commit |
|---|---|---|---|---|
| clang-tidy | registration_service.cpp:18 | performance-unnecessary-value-param | fixed: const std::string& | 3f0a9c2 |
| clang-tidy | registration_service.hpp:52 | cppcoreguidelines-avoid-const-or-ref-data-members | suppressed: the service is never copied or assigned; references express "must not be null" | 3f0a9c2 |
| cppcheck | event.cpp:12 | uninitMemberVar | fixed: int capacity_{0}; | 77b1e05 |
| cppcheck | report.cpp:27 | useStlAlgorithm | fixed: std::ranges::count_if | 77b1e05 |
| Final run: clang-tidy 0 warnings (1 suppressed with reason), cppcheck 0 findings. | ||||
- 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.mdfiles 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 creditScenario
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
- 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. - 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.
- 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. - 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--nowtime, the expected output and a Pass / Fail column. - 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.
- 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
| Component | Points | Full marks when… |
|---|---|---|
| Task 1 · Test plan for Sprint 2 | 10 | One-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 design | 20 | Partitions (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 tests | 25 | Injected 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 CI | 15 | Coverage 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 reports | 20 | Checklist-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 quality | 10 | Readable 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) | +20 | Chartered 45-minute exploratory session with classified notes and filed bugs; acceptance test script covering all scenarios, run and signed with the client. |
| Total | 100 (+20) |
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
- 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 - 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) - Work on branch
lab-09and open the pull requestLab-09 – <team name>againstmain, with the CI checks green and one approving review. Link the three bug issues and the coverage artifact in the PR description.