Practice Lab 16 — Media player interface with contract tests¶
Difficulty: Advanced · Estimated time: 5 hours · Module 8: Abstract Classes and Interfaces
An interface is a promise. This lab is about writing the promise down, and then writing a test that checks every implementation keeps it.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 5 hours |
| Module | Module 8 — Abstract Classes and Interfaces |
| Concepts practised | Interfaces as contracts, non-virtual interface idiom, pure virtual functions, capability interfaces, testing against an abstraction, substitutability |
| Prerequisite | Practice Lab 15 |
| Class design | yours — the contract is stated in behaviour; the shape of the interface is your design |
Scenario¶
A media library plays audio from three sources: a local file, a network stream, and a simulated device that never plays anything but records what it was asked to play. A fourth source is planned: a queue that chains sources together.
Every source does the same four things in the same order: open, play for a while, stop, and report its position and its state. They differ in how, and in what they refuse.
The team's previous interface had nine operations, and one implementer wrote five stubs that throw — because a network stream has no concept of "seek to frame". That experience is the reason this lab asks you to write the contract before the code.
Problem statement¶
Design a media source interface and implement three sources against it, plus a contract test that runs against any source.
- The operations a source must support, with their contracts written down: what each promises, what it refuses, and what state it leaves the source in.
- Three implementations: a local file, a network stream, and a device double that records what it was asked to do.
- A player that drives a source through a documented sequence and never asks which source it has.
- A contract test — one function taking a reference to the interface, running the same sequence and the same assertions for every implementation, and reporting pass or fail.
- A capability for seeking, which the network stream cannot honour — so it must be excluded by the type system, not by a run-time refusal.
- A composite source (a queue) that chains two sources and plays them in order, which must satisfy the same interface and the same contract test.
In contracts.md:
- The contract of each operation, in the promises / refuses / leaves form.
- Which invariant the contract test asserts, and which one it cannot.
- What the composite source revealed about your interface — every interface that supports composition gets at least one surprise.
Learning objectives¶
By the end of this lab you will be able to:
- Write an interface contract down, in terms of promises, refusals and state, before implementing it.
- Write one test that validates every implementation of an interface, and say what it cannot check.
- Split an interface by capability when one implementer cannot honour an operation.
- Implement a composite that satisfies the same interface, and explain the ordering and failure rules it needs.
- Explain why an implementation that writes throwing stubs is a design finding, not a diligence problem.
Functional requirements¶
- States are explicit and transitions are refused when illegal: opening an already-open source, playing a closed one, stopping one that is not playing.
- Every refusal is distinguishable and reproducible in the test table.
- Position is reported in seconds and never exceeds the source's duration; a source with unknown duration reports a documented unknown value.
- Playing past the end reports a documented end-of-source state rather than looping silently.
- The device double records the sequence of calls it received, so a test can assert the order of operations, not only their results.
- The player's sequence is: open, play until end or refusal, stop, report — and it prints one line per step.
- The contract test asserts at least six invariants and prints a pass/fail line per implementation, with a non-zero exit code if anything fails.
- Seeking is available only where it is supported, enforced by the type system.
- The composite source plays its children in order, and its own state reflects the child being played; if a child refuses, the composite reports it and stops.
- Every interface has a virtual destructor; sources are destroyed cleanly through the interface.
Constraints¶
- Your own layout, with a documented build command, and a header for the interfaces that includes no implementation header.
- No operation may be a stub that throws "not supported" — if an implementation cannot honour an operation, the operation is in the wrong interface. Record the check.
- No templates, no smart pointers.
- The contract test must compile once and run for all implementations; a copy per implementation is a failure of this lab.
- No warnings.
Starter task¶
In contracts.md, before writing code:
- Write the four verbs as operations with contracts. Include the state each one leaves the source in.
- Decide where seeking lives, given the network stream cannot honour it. Write both designs and choose.
- Write the contract test's assertion list: the six things you will assert for any source. Include at least one failure-path assertion.
- Predict what the composite source will need that a single source does not.
Implementation tasks¶
- Write the interface and the capability, with contracts at the declarations.
- Implement the three sources, including the recording double.
- Write the player and the contract test; run the test against all three.
- Implement the composite and run the same contract test against it.
- Deliberately break one implementation (skip a state transition) and confirm the contract test catches it. Keep the record of which assertion failed.
- Destroy sources through the interface and record the result.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Play a closed source | Refused; source unchanged |
| 2 | Open twice | Second open refused |
| 3 | Open, play, stop, close | All accepted, in that order |
| 4 | Stop a source that is not playing | Refused |
| 5 | Play past the end | End-of-source state reported, position capped at duration |
| 6 | Position on a source with unknown duration | The documented unknown value |
| 7 | Player against each source | The same sequence, with each source's own detail |
| 8 | Contract test against all three | All assertions pass; the pass lines are recorded |
| 9 | Contract test against the deliberately broken source | At least one assertion fails; the failing assertion is recorded |
| 10 | Seek on the network stream | Does not compile; the message is recorded |
| 11 | Composite of two sources | Plays in order; the same contract test passes |
| 12 | Composite where the first child refuses | The refusal is reported, the composite stops cleanly |
Boundary cases to run deliberately: a source with a 1-second duration; play of zero seconds; a composite of one source; a composite of an empty queue.
Expected behaviour¶
player with the local file
open: accepted, duration 12s
play 5s: position 5
play 20s: end of source at 12
stop: accepted
contract test: local file 6 of 6 assertions passed
contract test: network stream 6 of 6 assertions passed
contract test: recording device 6 of 6 assertions passed
contract test: broken source 4 of 6 assertions passed -> FAILED: "stop leaves state Stopped"
contract test: composite 6 of 6 assertions passed
seek on the network stream: does not compile ('NetworkStream' is not a 'Seekable')
Extension challenge¶
Add a repeat mode to the composite: when a child reaches the end, the composite restarts it a documented number of times before moving on.
Record in contracts.md:
- Which operations had to change, and whether the single-source implementations needed any edit. The count of edited files is the answer this extension is looking for.
- Where the repeat rule lives (the composite, the player, a decorator) and the cost of each option.
- Which contract-test assertion now needs to be aware of repeat mode, and whether a test can still be written once for every source.
- What the composite's position means while a child is repeating.
Debugging challenge¶
The contract test passes for every source, and the player still misbehaves:
void ContractTest(MediaSource& source) {
source.open();
source.play(5);
std::cout << "position: " << source.position() << "\n";
source.stop();
std::cout << "PASS\n"; // every implementation "passes"
}
Rewrite it and answer:
- Which six assertions are missing, and which requirement makes each one necessary?
- Why does printing a value prove nothing, and what did the course's Module 16 example demonstrate about that?
- Which assertion would catch "stop does not change the state"?
- What should the test do when it fails: print, throw, or return a failure count? Answer with how the test will be run in the next lab you write.
Reflection questions¶
- Where does seeking live now, and what would a caller who wants "seek if possible" have to do?
- Which contract could your test not check, and what would a test need to check it?
- The recording device asserts an order of calls. Why is an order assertion stronger than a result assertion, and when is it wrong to assert one?
- The composite satisfies the same interface as its parts. Name one rule it needed that a single source did not, and where you put it.
- A previous interface had nine operations and one implementer with five throwing stubs. Which of the nine would your design have kept, and why?
- If the fourth source were a network queue that can reorder items, what would your contract have to say about ordering?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ]
contracts.mdstates each operation's promise, refusal and resulting state - [ ] No implementation writes a throwing stub — checked and recorded
- [ ] Seeking is enforced by the type system, and the failed compile is recorded
- [ ] One contract test runs against every implementation, with assertions, and reports pass/fail
- [ ] A deliberately broken implementation is caught by the test, with the failing assertion recorded
- [ ] The composite passes the same contract test
- [ ] Every interface has a virtual destructor
- [ ] All twelve test cases run, with output recorded
- [ ] The debugging challenge was answered before being rewritten
Related: Lab index · Module 8 — Abstract Classes and Interfaces · Previous: Practice Lab 15 · Next: Practice Lab 17 — Library lending ownership model