% testing-fundamentals.tex — test pyramid, FIRST, AAA, SUT/DOC, behaviour vs implementation.
% Sources: docs/memos/testing-fundamentals.md, docs/school/cheatsheets/cheat-testing.md,
%          docs/school/explainers/explain-testing-philosophy.md, docs/school/qaa/testing.md.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/testing/testing-fundamentals.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=testing kind=concept level=core platform=general new=no round=round2-2026-09-23 topic=testing
% @tags: test-pyramid, ice-cream-cone, first-unit-tests, arrange-act-assert, given-when-then, sut, depended-on-component, unit-test, integration-test, code-coverage, regression-test, behaviour-vs-implementation
\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},
  sensitive=true, morecomment=[l]{//}, morecomment=[s]{/*}{*/}, morestring=[b]"}

\tikzset{
  lay/.style={draw=white, line width=1pt},
  lbl/.style={font=\scriptsize, align=center, inner sep=1pt},
  tl/.style={font=\footnotesize, text=black!75, align=left, inner sep=1pt, anchor=west},
}

\begin{document}

\sheettitle{Testing fundamentals — the pyramid, FIRST, AAA}{testing · memo}

\oneliner{Many fast \textbf{unit} tests ($\sim$70\,\%), fewer \textbf{integration} tests
($\sim$20\,\%), very few \textbf{UI/E2E} tests ($\sim$10\,\%). Going \emph{up} buys realism
and costs \textbf{speed, money, stability and failure localisation} — so the shape slopes.
Tests are an \emph{executable spec} whose real payoff is the \textbf{confidence to change code}.}

\vspace{3pt}
\noindent\begin{tikzpicture}[sheet]
  % ── the pyramid (base 6 cm, height 3.3 cm) ──
  \fill[sheetGreen!55, lay] (0,0) -- (6,0) -- (4.8,1.32) -- (1.2,1.32) -- cycle;
  \fill[sheetBlue!45, lay] (1.2,1.32) -- (4.8,1.32) -- (3.9,2.31) -- (2.1,2.31) -- cycle;
  \fill[sheetRed!55, lay] (2.1,2.31) -- (3.9,2.31) -- (3,3.3) -- cycle;
  \node[lbl] at (3,0.62) {\textbf{UNIT $\sim$70\,\%}\\ms each · one type, doubles for every DOC\\ViewModel · formatter · reducer};
  \node[lbl] at (3,1.78) {\textbf{INTEGRATION $\sim$20\,\%}\\VM + Repo + MockURLProtocol};
  \node[lbl] at (3,2.68) {\textbf{UI}\\$\sim$10\,\%};
  \node[font=\scriptsize\bfseries, text=sheetGreen!60!black] at (3,-0.25) {the test pyramid (Mike Cohn, 2009)};
  % ── slope axis ──
  \draw[->, very thick, sheetRed] (6.5,0.1) -- (6.5,3.2);
  \node[tl, text=sheetRed] at (6.6,3.15) {\textbf{up:} slower (minutes) · costlier to};
  \node[tl, text=sheetRed] at (6.6,2.83) {write + maintain · flakier · a red test};
  \node[tl, text=sheetRed] at (6.6,2.51) {could be any of 10 layers};
  \node[tl, text=sheetRed] at (6.6,2.19) {\emph{but} closest to what users see};
  \node[tl] at (6.6,1.72) {\textbf{tools:} unit = XCTest / Swift Testing};
  \node[tl] at (6.6,1.40) {integ.\ = MockURLProtocol, in-memory};
  \node[tl] at (6.6,1.08) {Core Data \;·\; UI = XCUITest};
  \node[tl, text=sheetGreen!55!black] at (6.6,0.64) {\textbf{down:} ms · cheap · deterministic};
  \node[tl, text=sheetGreen!55!black] at (6.6,0.32) {failure points at \emph{one} type};
  \node[tl, text=sheetGreen!55!black] at (6.6,0.0) {runs on every save (fast feedback)};
  % ── ice-cream cone ──
  \begin{scope}[xshift=12.3cm]
    \fill[sheetGreen!55, lay] (1.73,0.5) -- (2.47,0.5) -- (2.1,0) -- cycle;
    \fill[sheetBlue!45, lay] (1.32,1.05) -- (2.88,1.05) -- (2.47,0.5) -- (1.73,0.5) -- cycle;
    \fill[sheetRed!55, lay] (0.4,2.3) -- (3.8,2.3) -- (2.88,1.05) -- (1.32,1.05) -- cycle;
    \fill[sheetGrey!35] (2.1,2.3) ellipse (1.75 and 0.55);
    \node[lbl] at (2.1,2.5) {manual QA};
    \node[lbl] at (2.1,1.65) {\textbf{UI / E2E} (most)};
    \node[lbl] at (2.1,0.78) {integration};
    \node[lbl, font=\tiny] at (2.1,0.27) {unit};
    \node[font=\scriptsize\bfseries, text=sheetRed] at (2.1,-0.35) {ice-cream cone = ANTI-pattern};
    \node[lbl, text=sheetRed, font=\tiny] at (2.1,-0.62) {slow CI · flaky · poor diagnosis};
  \end{scope}
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}
  \item \textbf{Unit} — one type/function in isolation, \emph{no I/O}, every collaborator
        replaced by a double. \textbf{Integration} — several \emph{real} components together,
        only the outer boundary stubbed. \textbf{UI/E2E} — the built app driven through
        its screens (XCUITest runs in a \emph{separate process}).
  \item \textbf{Why it slopes (4 words):} \textbf{speed} · \textbf{cost} ·
        \textbf{flakiness} · \textbf{feedback} (how fast, how precisely a failure points at
        the bug). Fix an ice-cream cone by
        pushing logic \emph{down} into unit-testable ViewModels; keep UI tests for a few
        critical flows (login, checkout, onboarding).
  \item \textbf{SUT} = System Under Test. \textbf{DOC} (Depended-On Component) =
        \textbf{collaborator} — anything the SUT talks to (API client, store, clock). A unit
        test isolates the SUT by replacing DOCs with \emph{test doubles}.
  \item \textbf{Behaviour, not implementation:} assert observable outputs and effects, never
        private fields or ``which helper was called'' — those break on every refactor.
  \item \textbf{Don't test:} Apple's code (\texttt{URLSession}, \texttt{Codable} itself,
        SwiftUI layout), trivial getters, third-party internals. \textbf{Do test:} your logic,
        branches, boundaries, error paths, one regression test per fixed bug.
  \item \textbf{Coverage} = lines \emph{executed}, not behaviour \emph{verified} — a gap
        finder, not a quality score (100\,\% is possible with zero asserts).
\end{itemize}

\section{Example — AAA = Given / When / Then}
\begin{lstlisting}[language=SwiftSheet]
@testable import Shop
final class CartTests: XCTestCase {
  func test_givenTenPercentDiscount_whenTotal_thenReduced() {
    // Arrange / Given  — SUT + inputs + doubles
    let sut = Cart(items: [Item(price: 100)],
                   prices: StubPriceService())  // DOC -> double
    // Act / When      — ONE action
    sut.apply(discount: 0.10)
    // Assert / Then   — observable behaviour
    XCTAssertEqual(sut.total(), 90)
    // NOT: XCTAssertEqual(sut.lineCache.count, 1)  <- internals
  }
}
\end{lstlisting}
Swift Testing equivalent: \texttt{@Test("10\% discount") func discount()} +
\texttt{\#expect(sut.total() == 90)}; \texttt{\#require} stops the test on failure.

\columnbreak

\section{FIRST — five properties of a good unit test}
{\footnotesize
\begin{tabular}{@{}l>{\raggedright\arraybackslash}p{58mm}@{}}
\toprule
\textbf{\textcolor{sheetBlue}{F}ast} & milliseconds; you run thousands on every save \\
\textbf{\textcolor{sheetBlue}{I}ndependent} & no shared state; passes in \emph{any order} / in parallel (a.k.a.\ \emph{Isolated}) \\
\textbf{\textcolor{sheetBlue}{R}epeatable} & same result on any machine, offline, any day (no real \texttt{Date()}, network, randomness) \\
\textbf{\textcolor{sheetBlue}{S}elf-validating} & pass/fail by itself — no human reads a log \\
\textbf{\textcolor{sheetBlue}{T}imely} & written with/just before the code (TDD), not months later \\
\bottomrule
\end{tabular}}

\section{Interview traps}
\begin{itemize}
  \trap{Asked ``describe the pyramid'': give \textbf{three layers + numbers +
        the why}. ``No clue'' was the recorded answer — the minimum is
        \emph{70/20/10, cost and speed grow upward}.}
  \trap{``90\,\% of our tests are UI tests'' = \textbf{ice-cream cone} (inverted
        pyramid). Not ``great coverage''.}
  \trap{A test reading a file an earlier test wrote violates \textbf{I}ndependent
        (and \textbf{R}epeatable) — Xcode can run tests in parallel and in
        \emph{random order}.}
  \trap{``Hit the real API in unit tests for realism'' breaks F, I and R; realism
        belongs in a few contract/integration tests.}
  \trap{\textbf{One logical assertion} per test (several \texttt{XCTAssert}s on one
        outcome are fine); unrelated asserts = ambiguous failure.}
  \trap{A bug-fix test written \emph{after} the fix that you never saw red proves
        nothing — reproduce red first.}
  \trap{Flaky test = passes/fails with no code change; causes: \texttt{sleep}, shared
        state, order, real network, \texttt{Date()}/\texttt{UUID()}. It erodes trust in red.}
\end{itemize}

\section{Remember}
\textbf{``70 · 20 · 10 — up is Slow, Spendy, Shaky, Shallow-feedback.''}
Good test = \textbf{FIRST} + \textbf{AAA} + asserts \emph{what}, not \emph{how}.
Name = the spec line: \texttt{test\_<given>\_<when>\_<then>}.

\section{Likely questions}
\begin{enumerate}
  \item Pyramid layers + ratio? — unit 70, integration 20, UI 10.
  \item Why that shape? — higher = slower, costlier, flakier, worse localisation.
  \item What is the ice-cream cone? — mostly UI/manual tests; the anti-pattern.
  \item What does FIRST stand for? — Fast, Independent, Repeatable, Self-validating, Timely.
  \item SUT vs DOC? — thing tested vs the collaborators it depends on.
  \item Regression vs smoke? — pins a fixed bug vs shallow ``app launches'' check.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} test doubles · testable design \&
seams · async \& network testing · SOLID (DIP) · MVVM · TDD red-green-refactor · XCUITest}

\end{document}
