% testable-design-seams.tex — DI for tests, @testable import, singletons/static/Date()/UserDefaults,
% Feathers' seams, characterization tests, breaking dependencies in legacy code.
% Sources: docs/memos/testing-di-for-tests.md, docs/memos/testing-legacy-and-seams.md,
%          docs/school/explainers/explain-dependency-injection.md.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/testing/testable-design-seams.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=senior platform=general new=no round=round2-2026-09-23 topic=testing,architecture
% @tags: dependency-injection, seam, enabling-point, testable-import, characterization-test, legacy-code, feathers, composition-root, singleton, extract-interface, parameterize-constructor, service-locator
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

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

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=2pt, minimum height=5mm},
  bad/.style={sb, draw=sheetRed, fill=sheetRed!7, font=\scriptsize\ttfamily},
  good/.style={sb, draw=sheetGreen, fill=sheetGreen!10},
  abs/.style={sb, draw=sheetGreen!70!black, fill=sheetGreen!18, dashed, font=\scriptsize\itshape},
  tst/.style={sb, draw=sheetOrange, fill=sheetOrange!10},
  conf/.style={-{Latex[open,length=4pt]}, thick, draw=sheetGreen!70!black},
  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},
  pt/.style={font=\bfseries\small, anchor=west},
}

\begin{document}

\sheettitle{Testable design — DI, seams \& legacy code}{testing · memo}

\oneliner{A \textbf{seam} is ``a place where you can alter behaviour in your program without
editing in that place'' (Michael Feathers, \emph{Working Effectively with Legacy Code}, 2004);
its \textbf{enabling point} is where you choose the behaviour. In Swift the seam is a
\textbf{protocol} and the enabling point is the \textbf{\texttt{init}} — code that
\emph{reaches for} \texttt{.shared}, \texttt{Date()} or \texttt{UserDefaults.standard} has none.}

\vspace{3pt}
\noindent\begin{tikzpicture}[sheet]
  \draw[sheetGrey!40] (5.2,3.4) -- (5.2,-0.3);
  \draw[sheetGrey!40] (11.45,3.4) -- (11.45,-0.3);
  % ── BEFORE ──
  \node[pt] at (0,3.2) {\textcolor{sheetRed}{BEFORE} — hidden dependencies};
  \node[sb, minimum width=15mm, minimum height=16mm] (vm0) at (0.85,1.5) {Feed-\\ViewModel};
  \node[bad, minimum width=30mm] (a) at (3.6,2.55) {APIClient.shared};
  \node[bad, minimum width=30mm] (d) at (3.6,1.85) {Date()};
  \node[bad, minimum width=30mm] (u) at (3.6,1.15) {UserDefaults.standard};
  \node[bad, minimum width=30mm] (s) at (3.6,0.45) {Analytics.log(\_:) static};
  \foreach \n in {a,d,u,s} \draw[->, thick, sheetRed] (vm0.east) -- (\n.west);
  \node[lbl, text=sheetRed] at (2.6,-0.1) {not in the signature · no enabling point · real I/O};
  % ── AFTER ──
  \node[pt] at (5.3,3.2) {\textcolor{sheetGreen!60!black}{AFTER} — object seam};
  \node[good, minimum width=16mm, minimum height=10mm] (vm1) at (6.2,1.5) {FeedViewModel};
  \node[abs, minimum width=17mm] (p) at (8.55,1.5) {APIClient};
  \draw[flow] (vm1) -- node[lbl, above]{uses} (p);
  \node[lbl, text=sheetBlue] at (6.2,0.6) {enabling point:\\\texttt{init(api:)}};
  \node[good, minimum width=18mm] (live) at (10.4,2.6) {LiveAPIClient};
  \node[tst, minimum width=18mm] (stub) at (10.4,0.4) {StubAPI};
  \draw[conf] (live.west) -- (p.north east);
  \draw[conf] (stub.west) -- (p.south east);
  \node[lbl] at (10.4,3.05) {app / composition root};
  \node[lbl, text=sheetOrange] at (10.4,-0.05) {test target};
  \node[lbl, text=sheetGreen!55!black] at (9.7,1.5) {conforms};
  % ── Feathers' seam types ──
  \node[pt] at (11.55,3.2) {Feathers' 3 seam types};
  \node[ann] at (11.55,2.65) {\textbf{object} — swap via protocol/subclass};
  \node[ann] at (11.75,2.35) {$\leftarrow$ the Swift default};
  \node[ann] at (11.55,1.95) {\textbf{link} — swap a library/module at build};
  \node[ann] at (11.55,1.55) {\textbf{preprocessing} — \texttt{\#if DEBUG}};
  \node[pt] at (11.55,1.0) {Legacy loop};
  \node[ann, text=sheetBlue] at (11.55,0.5) {\textbf{characterize $\to$ seam $\to$ test}};
  \node[ann, text=sheetBlue] at (11.55,0.15) {\textbf{$\to$ refactor} (never refactor first)};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}
  \item \textbf{DI} = the type \emph{receives} collaborators instead of creating them.
        \textbf{Constructor} injection (preferred: required, \texttt{let}, never half-built) ·
        \textbf{property} (late/optional, e.g. delegate) · \textbf{method} (varies per call).
        Wire concretes in one \textbf{composition root} (\texttt{@main}/SceneDelegate).
  \item \textbf{Why these four hurt tests:}
        \begin{itemize}
          \item \emph{singleton} \texttt{.shared} — hidden, no seam; its state leaks
                between tests (breaks \textbf{I}ndependent);
          \item \emph{static call} — statically dispatched, nothing to substitute;
          \item \texttt{Date()}, \texttt{UUID()}, \texttt{random} — differ every run
                (breaks \textbf{R}epeatable);
          \item \texttt{UserDefaults.standard} — persists to \emph{disk} across tests and
                runs. Inject \texttt{UserDefaults(suiteName:)}; in \texttt{tearDown} call
                \texttt{removePersistentDomain(forName:)}.
        \end{itemize}
  \item \textbf{\texttt{@testable import App}} — tests see \texttt{internal} symbols (and may
        subclass/override \texttt{public} ones). Needs the module built with
        \emph{Enable Testability} (\texttt{-enable-testing}, on in Debug). \textbf{Never}
        exposes \texttt{private}/\texttt{fileprivate} — needing that means ``extract a type''.
  \item \textbf{Legacy code} = ``code without tests'' (Feathers). \textbf{Dilemma}: to change
        safely you need tests; to add tests you must change code. So the first edit is the
        smallest \emph{mechanical}, IDE/compiler-checked one that creates a seam.
  \item \textbf{Characterization test} (golden master): pins what the code does
        \emph{today}, bugs included. Recipe: assert a deliberately wrong value, run, copy the
        \emph{actual} from the failure message into the assertion.
\end{itemize}

\section{Dependency-breaking moves (Feathers)}
{\footnotesize
\begin{tabular}{@{}>{\raggedright\arraybackslash}p{19mm}>{\raggedright\arraybackslash}p{56mm}@{}}
\toprule
\textbf{Extract Interface} & protocol the concrete type already fits; depend on it \\
\textbf{Parameterize Constructor} & \texttt{init(api: APIClient = Live…)} — callers unchanged \\
\textbf{Wrap / Adapt} & own thin protocol around a static SDK or framework type \\
\textbf{Extract \& Override} & subclass SUT, override the I/O method — needs non-\texttt{final}; a stepping stone \\
\textbf{Sprout} Method/Class & new logic in a new, fully-tested unit; old code calls it \\
\textbf{Wrap} Method/Class & rename old, new one runs old + new behaviour before/after \\
\bottomrule
\end{tabular}}

\section{Remember}
\textbf{Receive, don't reach.} The four hidden deps: \textbf{S}ingleton · \textbf{S}tatic ·
\textbf{D}ate/UUID · \textbf{D}efaults — ``\textbf{SSDD}''. Legacy order:
\textbf{C}haracterize $\to$ \textbf{S}eam $\to$ \textbf{T}est $\to$ \textbf{R}efactor.

\columnbreak

\section{Example — parameterize the constructor}
\begin{lstlisting}[language=SwiftSheet]
protocol APIClient { func feed() async throws -> [Post] }
final class LiveAPIClient: APIClient { // old singleton
  static let shared = LiveAPIClient()
  func feed() async throws -> [Post] { /* URLSession */ [] } }
final class FeedViewModel {
  private let api: APIClient
  private let now: () -> Date
  private let defaults: UserDefaults
  init(api: APIClient = LiveAPIClient.shared,   // prod call sites
       now: @escaping () -> Date = Date.init,   //   stay unchanged
       defaults: UserDefaults = .standard) {
    self.api = api; self.now = now; self.defaults = defaults } }
// test: FeedViewModel(api: StubAPI(),
//         now: { Date(timeIntervalSince1970: 0) },
//         defaults: UserDefaults(suiteName: #function)!)
\end{lstlisting}
{\footnotesize The brief form \texttt{init(api: APIClient = .shared)} compiles when
\texttt{APIClient} is the \emph{concrete class}; with a protocol, name the type
(\texttt{LiveAPIClient.shared}).}

\section{Interview traps}
\begin{itemize}
  \trap{\textbf{Live-default leak}: a test that forgets one argument silently gets the
        \emph{real} \texttt{LiveAPIClient} $\to$ network in tests. Pass every dep in tests.}
  \trap{Making things \texttt{public} ``for tests'' — \texttt{@testable} already opens
        \texttt{internal}; test \texttt{private} logic through behaviour.}
  \trap{Swapping \texttt{static var shared} in a test without resetting it in
        \texttt{tearDown}/\texttt{addTeardownBlock} = order-dependent flakes.}
  \trap{``Refactor the class first, then test'' — refactoring untested code changes
        behaviour silently. Characterize first.}
  \trap{Service Locator (\texttt{Container.resolve(X.self)} inside the body) is \emph{not}
        DI — the dependency is still hidden.}
  \trap{8 init parameters is an \textbf{SRP} smell, not a DI problem — split the type.}
  \trap{DI $\neq$ DIP: DI is the technique; DIP says the \emph{protocol belongs to the
        high-level side}.}
\end{itemize}

\section{Likely questions}
\begin{enumerate}
  \item What is a seam? — alter behaviour without editing there; has an enabling point.
  \item Test code that uses \texttt{URLSession.shared}? — protocol + defaulted init param.
  \item What does \texttt{@testable} do? — exposes \texttt{internal}; never \texttt{private}.
  \item Characterization test? — pins current behaviour before you change it.
  \item Why is \texttt{Date()} a problem? — non-repeatable; inject \texttt{() -> Date}.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} SOLID (DIP) · dependency injection ·
test doubles · testing fundamentals (FIRST) · async \& network testing (injected clock) ·
Humble Object · SwiftUI \texttt{@Environment}}

\end{document}
