% xctest-and-swift-testing.tex — XCTestCase lifecycle, assertions, leak test, measure, Swift Testing.
% Sources: docs/memos/testing-xctest.md, docs/memos/testing-performance-and-strategy.md,
%          docs/school/qaa/ios-memory.md (Q17 — student PARTIAL on the leak test).
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/testing/xctest-and-swift-testing.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=testing kind=api level=core platform=apple new=no round=round2-2026-09-23 topic=testing
% @tags: xctest, xctestcase, setupwitherror, addteardownblock, xctunwrap, swift-testing, expect-macro, require-macro, leak-test, xctmetric, xctestplan, parameterized-tests
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

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

\tikzset{
  st/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  once/.style={st, draw=sheetBrown, fill=sheetBrown!10},
  per/.style={st, draw=sheetBlue, fill=sheetBlue!8},
  tst/.style={st, draw=sheetGreen, fill=sheetGreen!14, font=\scriptsize\bfseries},
  tdn/.style={st, draw=sheetOrange, fill=sheetOrange!10},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
}

\begin{document}

\sheettitle{XCTest \& Swift Testing — lifecycle, asserts, leak test}{testing · memo}

\oneliner{XCTest makes a \textbf{fresh \texttt{XCTestCase} instance per test method} and wraps
each one in \texttt{setUp} → test → \texttt{tearDown}; Swift Testing (\texttt{import Testing},
Xcode 16+) does the same with a \texttt{@Suite}'s \texttt{init}/\texttt{deinit}, \texttt{@Test}
and \texttt{\#expect}/\texttt{\#require}. The leak test = \textbf{weak ref → drop the strong →
assert \texttt{nil}}.}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}\raggedright
  \item \textbf{Discovery:} subclass \texttt{XCTestCase}; every instance method named
        \texttt{test…}, no args, returns \texttt{Void} (may be \texttt{throws}/\texttt{async}) runs.
        Order is \emph{not} a contract — tests must be independent.
  \item \textbf{Lifecycle:} \texttt{class func setUp()} \textbf{once} per class → for each test:
        \emph{new instance} → \texttt{setUpWithError()} → \texttt{setUp()} → \texttt{test…} →
        \texttt{addTeardownBlock}s (\textbf{LIFO}) → \texttt{tearDownWithError()} →
        \texttt{tearDown()} → … \texttt{class func tearDown()} once.
  \item Prefer the \texttt{WithError} variants: they \texttt{throws}, so setup can \texttt{try}
        and a throw fails the test cleanly (no \texttt{try!}).
  \item Instance state is fresh, but \textbf{statics, singletons, \texttt{UserDefaults}, files}
        are not — and the instance lives on in the runner: \textbf{nil the SUT in \texttt{tearDown}}.
  \item \textbf{Asserts:} \texttt{XCTAssertEqual/NotEqual} · \texttt{True/False} ·
        \texttt{Nil/NotNil} · \texttt{Identical} (\texttt{===}) · \texttt{GreaterThan…} ·
        \texttt{XCTAssertThrowsError(expr)\{ err in … \}} · \texttt{XCTAssertNoThrow} ·
        \texttt{XCTFail("msg")} (unconditional) · \texttt{try XCTUnwrap(opt)} (fail+stop, not crash) ·
        \texttt{throw XCTSkip} / \texttt{XCTSkipIf}. All take \texttt{message}, capture
        \texttt{file}/\texttt{line}.
  \item \textbf{Performance:} \texttt{measure \{ \}} runs the block several times, compares the average to a stored
        \textbf{baseline} (per device+OS). \texttt{measure(metrics:)} with \texttt{XCTClockMetric} ·
        \texttt{XCTCPUMetric} · \texttt{XCTMemoryMetric} · \texttt{XCTStorageMetric} ·
        \texttt{XCTOSSignpostMetric}. Baselines don't transfer between machines.
  \item \textbf{Test plan} (\texttt{.xctestplan}): configurations (locale, env, sanitizers),
        random order, parallel, repetitions, coverage.
\end{itemize}

\section{Example — the leak test (you were PARTIAL)}
\begin{lstlisting}[language=SwiftSheet]
func test_viewModel_isDeallocated() {
  weak var weakRef: FeedViewModel?
  do {                              // inner scope = release point
    let sut = FeedViewModel(api: StubAPI())
    weakRef = sut                   // observe WITHOUT retaining
    sut.start()                     // code that may cycle
  }                                 // last strong ref gone here
  XCTAssertNil(weakRef, "leak: retain cycle keeps VM alive")
}
// reusable form — run AFTER the test body, for every makeSUT():
func trackForLeaks(_ o: AnyObject, line: UInt = #line) {
  addTeardownBlock { [weak o] in
    XCTAssertNil(o, "leaked", line: line) }
}
\end{lstlisting}

\columnbreak

\section{Picture — one test class, two tests}
\noindent\begin{tikzpicture}[sheet, node distance=2.2mm,
    st/.append style={minimum height=7.5mm, minimum width=9mm}]
  \node[once] (cs) at (0,0) {class\\setUp};
  \node[per, right=of cs] (i1) {new\\instance};
  \node[per, right=of i1] (s1) {setUp\\WithError};
  \node[tst, right=of s1] (t1) {testA};
  \node[tdn, right=of t1] (b1) {teardown\\blocks};
  \node[tdn, right=of b1] (d1) {tearDown\\WithError};
  \draw[flow] (cs) -- (i1); \draw[flow] (i1) -- (s1); \draw[flow] (s1) -- (t1);
  \draw[flow] (t1) -- (b1); \draw[flow] (b1) -- (d1);
  \node[per, below=6mm of i1] (i2) {new\\instance};
  \node[per, right=of i2] (s2) {setUp\\WithError};
  \node[tst, right=of s2] (t2) {testB};
  \node[tdn, right=of t2] (b2) {teardown\\blocks};
  \node[tdn, right=of b2] (d2) {tearDown\\WithError};
  \node[once, left=of i2] (ct) {class\\tearDown};
  \draw[flow] (d1.south) -- ++(0,-2.2mm) -| (i2.north);
  \draw[flow] (i2) -- (s2); \draw[flow] (s2) -- (t2); \draw[flow] (t2) -- (b2);
  \draw[flow] (b2) -- (d2);
  \draw[flow] (d2.south) -- ++(0,-2.2mm) -| (ct.south);
  \node[lbl, anchor=north west, text=sheetBrown, align=left] at ($(ct.south west)+(0,-3.4mm)$)
    {brown = once per class · blue = per test · green = your test · orange = teardown\\
     plain \texttt{setUp()} runs right after \texttt{setUpWithError}; plain \texttt{tearDown()} last\\
     teardown blocks run LIFO, \emph{before} \texttt{tearDownWithError}/\texttt{tearDown}\\
     \textcolor{sheetRed}{testA and testB never share instance state — but DO share statics, singletons, disk}};
\end{tikzpicture}

\section{XCTest vs Swift Testing}
{\footnotesize
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{14mm}>{\raggedright\arraybackslash}p{27mm}>{\raggedright\arraybackslash}p{35mm}@{}}
\toprule
 & \textbf{XCTest} & \textbf{Swift Testing} \\
\midrule
declare & \texttt{func test…()} in \texttt{XCTestCase} & \texttt{@Test func any()} (free or in type) \\
group & the class & \texttt{@Suite struct} (fresh per test) \\
setup/teardown & \texttt{setUp}/\texttt{tearDown} & \texttt{init} / \texttt{deinit} \\
soft check & \texttt{XCTAssertEqual(a,b)} & \texttt{\#expect(a == b)} — shows both values \\
hard check & \texttt{try XCTUnwrap(x)} & \texttt{try \#require(x)} (throws, stops) \\
errors & \texttt{XCTAssertThrowsError} & \texttt{\#expect(throws: E.self)\{…\}} \\
async wait & \texttt{XCTestExpectation} & \texttt{await confirmation\{…\}} \\
table tests & loop (one result) & \texttt{@Test(arguments: […])} — 1 case each \\
traits & — & \texttt{.disabled} \texttt{.enabled(if:)} \texttt{.tags} \texttt{.timeLimit} \texttt{.bug} \texttt{.serialized} \\
parallel & opt-in, sim clones & \textbf{default}, in-process \\
UI / perf & XCUITest, \texttt{measure} & \textbf{not supported} — stay in XCTest \\
\bottomrule
\end{tabular}}

\section{Interview traps}
\begin{itemize}
  \trap{\texttt{deinit\{print\}} and Instruments are \emph{debugging}, not a \textbf{test} — the
        automated check is weak-ref-then-\texttt{XCTAssertNil}.}
  \trap{\texttt{trackForLeaks} on a SUT stored in \texttt{var sut} \textbf{always fails}:
        teardown blocks run \emph{before} \texttt{tearDown} nils it. Create it in a local
        \texttt{makeSUT()}.}
  \trap{\texttt{XCTUnwrap} vs \texttt{!}: a force-unwrap crash kills the \emph{whole runner};
        \texttt{XCTUnwrap} fails one test.}
  \trap{Porting \texttt{XCTAssertEqual(a,b)} as \texttt{\#expect(a,b)} — idiomatic is
        \texttt{\#expect(a == b)}. Both frameworks coexist in one target.}
  \trap{\texttt{measure} on shared CI flakes — baselines are per device+OS.}
\end{itemize}

\section{Remember}
\textbf{Weak → scope → nil.} \texttt{\#expect} continues, \texttt{\#require} stops. New instance
per test; teardown blocks LIFO before \texttt{tearDown}.

\section{Likely questions}
\begin{enumerate}
  \item Order of setUp variants? — class setUp once; per test setUpWithError → setUp → test → blocks → tearDown(WithError).
  \item Test a VM for retain cycles? — \texttt{weak var} ref, drop strong in scope, \texttt{XCTAssertNil}.
  \item \texttt{\#expect} vs \texttt{\#require}? — soft (records, continues) vs hard (throws).
  \item Why parameterized over a loop? — each argument reported and re-runnable alone.
  \item Measure memory in a test? — \texttt{measure(metrics: [XCTMemoryMetric()])}.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} testing fundamentals (pyramid, FIRST) ·
ARC ownership · Instruments (Leaks, Allocations) · testing ViewModels / UI / snapshot ·
flaky tests \& CI}

\end{document}
