% test-doubles.tex — Dummy / Stub / Spy / Mock / Fake (Meszaros), state vs behaviour
% verification, hand-rolled Swift doubles, over-mocking.
% Sources: docs/memos/testing-doubles.md, docs/memos/testing-mocking-frameworks.md,
%          docs/school/mock-interviews/mock-testing.md.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/testing/test-doubles.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=testing kind=pattern level=core platform=general new=no round=round2-2026-09-23 topic=testing,patterns
% @tags: test-double, dummy, stub, spy, mock, fake, state-verification, behaviour-verification, over-mocking, meszaros, classicist-vs-mockist, mock-codegen
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{SwiftSheet}{
  morekeywords={protocol,class,final,struct,enum,func,var,let,weak,init,import,
    if,else,return,guard,self,nil,try,await,async,throws,private,some,override,
    true,false,Void,String,Bool,Decimal,Int,Double,UInt,StaticString},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/}, morestring=[b]"}

\tikzset{
  db/.style={box, font=\small\bfseries, minimum width=16mm, minimum height=6mm},
  inp/.style={db, draw=sheetOrange, fill=sheetOrange!10},
  outp/.style={db, draw=sheetGreen!70!black, fill=sheetGreen!12},
  dum/.style={db, draw=sheetGrey, fill=black!4, dashed},
  lbl/.style={font=\scriptsize, text=black!80, inner sep=1pt, align=center},
  ann/.style={font=\scriptsize, text=black!80, inner sep=1pt, anchor=west, align=left},
}

\begin{document}

\sheettitle{Test doubles — Dummy · Stub · Spy · Mock · Fake}{testing · memo}

\oneliner{A \textbf{test double} (Gerard Meszaros, \emph{xUnit Test Patterns}, 2007) stands
in for a \textbf{DOC} so the SUT can be tested alone. \textbf{Stub}/\textbf{Fake}
\emph{control indirect inputs} $\to$ \textbf{state verification}; \textbf{Spy}/\textbf{Mock}
\emph{observe indirect outputs} $\to$ \textbf{behaviour verification}; a \textbf{Dummy} just
fills a parameter. The real distinction is \emph{direction} and \emph{who verifies}.}

\vspace{3pt}
\noindent\begin{tikzpicture}[sheet]
  \node[box, minimum width=15mm, minimum height=13mm, font=\small\bfseries] (t) at (0.8,1.6) {Test};
  \node[box, minimum width=20mm, minimum height=13mm, font=\small\bfseries, fill=sheetBlue!15] (s) at (5.0,1.6) {SUT};
  \draw[flow] ([yshift=3mm]t.east) -- node[lbl, above]{\textbf{1} act: direct input} ([yshift=3mm]s.west);
  \draw[flow] ([yshift=-3mm]s.west) -- ([yshift=-3mm]t.east);
  \node[lbl] at (2.9,1.08) {direct output / state};
  \node[lbl, text=sheetOrange] at (2.9,0.78) {test: \texttt{XCTAssertEqual(sut.state, …)}};
  % doubles
  \node[inp] (stub) at (9.2,3.05) {Stub};
  \node[inp] (fake) at (9.2,2.2) {Fake};
  \node[outp] (spy) at (9.2,1.0) {Spy};
  \node[outp] (mock) at (9.2,0.15) {Mock};
  \node[dum] (dum) at (5.0,0.1) {Dummy};
  \draw[hot] (stub.west) -- ([yshift=4mm]s.east);
  \draw[hot, <->] (fake.west) -- ([yshift=1mm]s.east);
  \draw[->, thick, sheetGreen!60!black] ([yshift=-2mm]s.east) -- (spy.west);
  \draw[->, thick, sheetGreen!60!black] ([yshift=-4mm]s.east) -- (mock.west);
  \node[lbl, text=sheetOrange, font=\scriptsize\bfseries] at (6.6,3.05) {indirect INPUT $\to$ SUT};
  \node[lbl, text=sheetGreen!50!black, font=\scriptsize\bfseries] at (7.0,-0.2) {SUT $\to$ indirect OUTPUT};
  \draw[dashed, sheetGrey] (dum.north) -- (s.south);
  % annotations
  \node[ann] at (10.2,3.05) {canned answer: \texttt{return .success(user)}};
  \node[ann] at (10.2,2.2) {working shortcut: in-memory store, no disk};
  \node[ann] at (10.2,1.0) {records calls; \textbf{test} asserts \emph{after} the act};
  \node[ann] at (10.2,0.15) {expectations set \emph{before}; \textbf{mock} verifies \emph{itself}};
  \node[lbl, text=sheetGrey] at (3.2,0.1) {passed, never used};
  % verification brackets
  \draw[decorate, decoration={brace, amplitude=4pt}, thick, sheetOrange] (15.55,3.35) -- (15.55,1.9)
       node[midway, right=4pt, lbl, text=sheetOrange, align=left]{\textbf{STATE}\\verification};
  \draw[decorate, decoration={brace, amplitude=4pt}, thick, sheetGreen!60!black] (15.55,1.3) -- (15.55,-0.15)
       node[midway, right=4pt, lbl, text=sheetGreen!50!black, align=left]{\textbf{BEHAVIOUR}\\verification};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works — Meszaros' five}
\begin{itemize}
  \item \textbf{Dummy} — passed to satisfy a signature, \emph{never used}. Make it
        \texttt{fatalError()} so a surprise call is loud.
  \item \textbf{Stub} — returns \emph{canned answers}; gives the test a \emph{control point
        for indirect inputs} (success, error, empty). You then assert the SUT's state/output.
  \item \textbf{Spy} — a stub that also \emph{records} calls/arguments; the \emph{test}
        inspects the log \emph{after} the act. Most Swift ``mocks'' are really spy+stub hybrids.
  \item \textbf{Mock} — \emph{pre-programmed with expectations} of the calls it should
        receive; it \emph{verifies itself} (fails on unexpected/missing calls).
  \item \textbf{Fake} — a \emph{working, simplified} implementation (in-memory repository,
        in-memory Core Data store). Has real logic, so it can have real bugs.
  \item \textbf{State verification} (Stub/Fake): assert \emph{what} the SUT ended up with —
        survives refactors; the default. \textbf{Behaviour verification} (Spy/Mock): assert
        \emph{how} it talked to a DOC — use only when the \emph{call is the contract}
        (analytics sent, logout called, email sent \emph{once}).
\end{itemize}

\section{Example — one protocol, five doubles}
\begin{lstlisting}[language=SwiftSheet]
protocol Mailer { func send(to: String) -> Bool }

struct DummyMailer: Mailer {              // fills a slot
  func send(to: String) -> Bool { fatalError("unused") } }
struct StubMailer: Mailer {               // canned INPUT
  var result = false
  func send(to: String) -> Bool { result } }
final class SpyMailer: Mailer {           // records OUTPUT
  private(set) var sent: [String] = []
  func send(to: String) -> Bool { sent.append(to); return true } }
\end{lstlisting}
\begin{lstlisting}[language=SwiftSheet]
final class MockMailer: Mailer {          // expects + self-checks
  private let expected: [String]; private var got: [String] = []
  init(expect: [String]) { expected = expect }
  func send(to: String) -> Bool { got.append(to); return true }
  func verify(file: StaticString = #filePath, line: UInt = #line) {
    XCTAssertEqual(got, expected, file: file, line: line) } }
final class FakeMailer: Mailer {          // real logic, no SMTP
  private(set) var outbox: [String] = []
  func send(to: String) -> Bool {
    guard to.contains("@") else { return false }
    outbox.append(to); return true } }
\end{lstlisting}

\section{Remember}
\textbf{Dummy \emph{fills} · Stub \emph{feeds} · Spy \emph{notes} · Mock \emph{judges} · Fake
\emph{works}.} Default: classicist (Detroit) — real objects + stubs/fakes, state
verification; mockist (London) interaction checks only at true seams.

\columnbreak

\section{Using them — state vs behaviour}
\begin{lstlisting}[language=SwiftSheet]
// STATE: the stub steers, assert the SUT's result
let sut = Signup(mailer: StubMailer(result: false))
sut.register("ada@x.io")
XCTAssertEqual(sut.state, .emailFailed)
// BEHAVIOUR: the outgoing call IS the requirement
let spy = SpyMailer()
Signup(mailer: spy).register("ada@x.io")
XCTAssertEqual(spy.sent, ["ada@x.io"])   // once, in order
\end{lstlisting}

\section{Why iOS teams hand-roll}
\begin{itemize}
  \item Mockito (Java) builds mocks at \emph{runtime} with proxies/bytecode; OCMock uses
        the Obj-C \emph{runtime} (message forwarding) — works only for
        \texttt{NSObject}/\texttt{@objc}. Swift is largely \emph{statically dispatched}, and
        \texttt{Mirror} is read-only — \textbf{no runtime mocking}.
  \item So Swift frameworks must \textbf{generate code}: Cuckoo, Mockolo (Uber), Sourcery
        \texttt{AutoMockable}, Mockingbird, or Swift~5.9 \textbf{macros}. Cost: extra build
        phase, generated-file churn, breakage on Xcode/Swift upgrades, opaque code.
  \item A protocol double is 3--5 lines, \emph{compiler-checked}, refactor-safe, readable
        in review. Lighter still: a \textbf{struct of closures}
        (\texttt{var fetch: (Int) async throws -> User}).
\end{itemize}

\section{Interview traps}
\begin{itemize}
  \trap{\textbf{Stub vs Mock} is the \#1 slip: stub = \emph{inputs}, the \emph{test}
        asserts state; mock = \emph{outputs}, the \emph{double} verifies interactions.}
  \trap{\textbf{Over-mocking}: mocking everything (even value types) and asserting call
        sequences $\to$ tests pass while the app is broken, and break on every refactor.
        Mock only side-effecting boundaries: network, disk, clock, analytics.}
  \trap{\textbf{Tautology}: stub returns \texttt{User(id:1)}, assert \texttt{User(id:1)} —
        the decoding you care about never ran. Stub the \emph{raw bytes}.}
  \trap{\textbf{Partial mock} (subclass the SUT, override some methods) — tests a
        different class; extract the dependency instead.}
  \trap{``Don't mock what you don't own'' — wrap \texttt{URLSession} behind \emph{your}
        protocol; never subclass framework types.}
\end{itemize}

\section{Likely questions}
\begin{enumerate}
  \item Stub vs mock? — canned input + state check vs expected calls, self-verified.
  \item Spy vs mock? — spy: test asserts after; mock: expectations before, checks itself.
  \item When a fake? — read-after-write / multi-call flows (in-memory repo).
  \item Why no Mockito in Swift? — static dispatch, no runtime proxies $\to$ codegen only.
  \item When behaviour verification? — when the interaction itself is the requirement.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} testing fundamentals (SUT/DOC,
pyramid) · testable design \& seams · async \& network testing (MockURLProtocol) · SOLID (DIP) ·
protocols \& dispatch}

\end{document}
