Testing in C++ — discipline against silent bugs
In runtime-rich languages, the interpreter catches your mistake. In C++ the mistake either crashes or quietly corrupts memory and shows up an hour later under load. So tests in C++ are not "extra politeness" — they are the first line of defense. This page collects the vocabulary and tooling interviewers ask about.
Topic map
- Unit vs integration vs TDD — what each level actually verifies and why mixing them hurts.
- Mocks, fakes, stubs — three different kinds of test doubles, and why they are not interchangeable.
- Frameworks — GoogleTest, Catch2, doctest: what they give you and where they diverge.
- Code coverage — line / branch / condition coverage, and what they do not prove.
- Testing private methods — why this is usually a design smell, and the rare cases where it is justified.
Unit vs integration vs TDD
A unit test verifies one function or class in isolation. Dependencies are replaced with doubles. The goal is to catch logic errors. It must be deterministic, fast (milliseconds), and must not touch disk, network, or wall-clock time.
An integration test verifies the wiring of several modules with real dependencies: a real database, a real HTTP client, a real filesystem. Slower and flakier, but catches contract bugs.
TDD (Test-Driven Development) is the red → green → refactor loop: failing test first, then minimal implementation, then refactor. TDD is about design, not coverage: the test becomes the first client of the API and surfaces awkward dependencies.
Mocks, fakes, stubs
Three kinds of test doubles. Confusing them is a classic interview slip.
- Stub — returns a canned response, does not check calls. Use when you just need a fixed answer from a dependency.
- Fake — a simplified but functional implementation (an in-memory DB instead of PostgreSQL). Use in integration tests.
- Mock — records every call and asserts that the expected interactions happened. Use when you are verifying interaction, not state.
In GoogleMock, EXPECT_CALL builds a mock; a hand-rolled class FakeStore : public IStore is a fake; ON_CALL(...).WillByDefault(Return(42)) is a stub. Over-using mocks couples tests to code structure and breaks them on every refactor.
Frameworks
GoogleTest — the industry de-facto standard. Rich matchers, GMock for doubles, parameterized tests, death tests. Downside: substantial infrastructure, slow compile.
Catch2 — header-only, terse macro-based syntax (TEST_CASE / SECTION). Good for libraries and small projects. Slower test discovery.
doctest — the fastest to compile, minimalistic.
All three integrate with CMake via find_package or FetchContent. Do not roll your own framework — it will be worse than all three and lack CI integration.
Code coverage
gcov / llvm-cov collect execution statistics. Three flavors:
- Line coverage — which lines ran at least once. Weakest metric.
- Branch coverage — which
if/switchbranches were taken. Better, butif (a && b)has four combinations and only two branches. - Condition coverage (MC/DC) — every combination of conditions inside an expression. Used in aviation/automotive code.
⚠️ High coverage does not guarantee correctness — it guarantees the tests executed the code. A test with no EXPECT_* / ASSERT_* gives 100% coverage and zero checks.
Testing private methods
Direct advice: do not. If a private method needs its own test, that is a signal it is actually a public unit of behavior — extract it to its own class with a real public API.
When you genuinely cannot:
friend class FooTestin the production code — breaks encapsulation, but locally;- inherit and expose via a
publicwrapper only in the test; private:→protected:plus a test-only subclass — the cleanest option.
Never write #define private public before an #include. It is UB by the standard and will eventually break in surprising ways.
Common traps
| Mistake | Consequence |
|---|---|
| One test asserts several unrelated things | A failure does not tell you what broke |
| Mock everywhere a fake would do | Tests break on every refactor |
| Coverage as a KPI | The team writes meaningless tests to inflate the number |
sleep(1) for synchronization in tests | Flaky tests, slow CI |
Testing private methods via friend | Encapsulation is sacrificed, design does not improve |
| Global state shared between tests | Order of execution affects results |
| TDD interpreted as "write tests afterward" | Loses the main benefit — design through tests |
Interview relevance
Testing is almost always a middle/senior portion of a C++ interview in industry (backend, embedded; less so gamedev). The interviewer is not checking whether you know GTest syntax — they check whether you can distinguish unit from integration, pick the right kind of double, and explain why "100% coverage" is a bad goal.
What the interviewer is actually checking:
- Can you explain mock vs fake vs stub without peeking?
- Do you understand that a unit test must not touch DB, network, or time?
- Do you know that coverage measures execution, not verification?
- Do you know when TDD helps and when it gets in the way (e.g., research code)?
Typical wrong answer: "A mock is when we replace a class with a stub." That is the definition of a stub, not a mock. A mock asserts interaction; a stub does not.