<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>The Mythical 10x Engineer on The Smidt Blog</title><link>https://blog.smidt.dev/series/the-mythical-10x-engineer/</link><description>Recent content in The Mythical 10x Engineer on The Smidt Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 12 Oct 2026 10:00:00 -0400</lastBuildDate><atom:link href="https://blog.smidt.dev/series/the-mythical-10x-engineer/index.xml" rel="self" type="application/rss+xml"/><item><title>The Mythical 10x Engineer</title><link>https://blog.smidt.dev/posts/0010/</link><pubDate>Mon, 12 Oct 2026 10:00:00 -0400</pubDate><guid>https://blog.smidt.dev/posts/0010/</guid><description>&lt;h2 id="intro">Intro
&lt;/h2>&lt;p>Pfft,&lt;/p>
&lt;p>What a weird new environment we ended up in. The codebase sits at the bottom of a valley, ten years old, like an olive tree, aged and grown. But the ground around its roots is black and sticky. A tarpit. The tree is slowly being swallowed by it, and I suspect it has been for years.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Oh no, are we going to maintain this till the end of its life?&lt;/p>
&lt;/blockquote>
&lt;p>No Bobby, it can barely stand upright.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Let me try to pull out a module to make it a separate dependency. Darn, it took half the repository with it.&lt;/p>
&lt;/blockquote>
&lt;p>Yeah, it is a big ball of mud. If we keep on working on it we will sink into the tar together with the tree. We will be bogged down.
Are there any tests?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: I found some tests.&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_create_user&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Mock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">side_effect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">41&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Tommy&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kc">None&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">create_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Tommy&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">call_count&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assert_any_call&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;SELECT MAX(id) AS id FROM users&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">assert_any_call&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO users (id, name) VALUES (?, ?)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;Tommy&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So what does it test?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: User creation. Let me look at how it is implemented.&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">create_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">latest&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;SELECT MAX(id) AS id FROM users&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">latest&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO users (id, name) VALUES (?, ?)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">user_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">finally&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;SELECT id, name FROM users WHERE name = ?&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">user&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;INSERT INTO user_events (user_id, event) VALUES (?, ?)&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s2">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">execute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> INSERT INTO user_events (at, user_id, event)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> VALUES (?, ?, ?)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s2"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">(&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">user_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;failed&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="ne">RuntimeError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;User was not created&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Right, so we don&amp;rsquo;t spin up a database? We just check how often we call &lt;code>db.execute&lt;/code>?
Who even dared to commit this?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">test: increase coverage from 41% to 78%
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Added tests for the database layer and failure handling.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">assisted-by: Claude
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">committed-by: Tommy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Bobby looks impressed.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: What an impressive commit. Three thousand lines of code and a thirty-seven percent increase in test coverage.&lt;/p>
&lt;/blockquote>
&lt;p>Yeah, I suppose Tommy really was a 10x developer. A negative 10x. Tommy left before we arrived, but you hear the stories at the coffee machine.
He pushed his PR, didn&amp;rsquo;t add any tests and merged. He didn&amp;rsquo;t even deploy it to RC for testing&amp;hellip;
They eventually had to require a PR review and disable the break-glass procedure to merge to main.
For their own sanity. They were lucky he didn&amp;rsquo;t know about force pushes.&lt;/p>
&lt;p>Bobby was already getting distracted by something else in the codebase.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Wouldn&amp;rsquo;t it be more efficient if we fixed this loop?
Someone put a lot of sleeps here.
I&amp;rsquo;ll open a PR since it seems like a good improvement.&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>PR rejected: DO NOT TOUCH THIS&lt;/strong>&lt;/p>
&lt;p>We see a man running at us, an old man with a long gray beard. He has probably worked here for the last 40 years.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Old man&lt;/strong>: How could you be so careless, trying to change things!
Fixing this loop to be faster would have caused other scripts to fall behind and create race conditions.&lt;/p>
&lt;/blockquote>
&lt;p>Okay old man, calm down, we will leave this tree alone.&lt;/p>
&lt;p>We can&amp;rsquo;t keep patching the old tree anyway. We&amp;rsquo;ve already seen what happens when we shake one branch. Another branch is propped up with an old piece of wood. I doubt it could even carry a strangler fig, and the tar would probably swallow the fig too.&lt;/p>
&lt;p>So now we are here, to clean up this mess.&lt;/p>
&lt;p>But where do we even start?&lt;/p>
&lt;p>We need a plan. We need to understand the system.&lt;/p>
&lt;h2 id="part-i-finding-the-trail">Part I: Finding The Trail
&lt;/h2>&lt;p>Fortunately someone marked the trail before us.&lt;/p>
&lt;p>Somewhere in the company is another codebase. It&amp;rsquo;s doing something different, but it was easy to work in.
It was built by my old mentor, Mr. T.R., who was to me what I am to you now, Bobby. Small world: years after he taught me, he ended up at this company too. By the time I arrived he had already left again, but his code was still here.&lt;/p>
&lt;p>Mr. T.R. had the habit of not chasing the latest and greatest. He had the habit of keeping things boring.&lt;/p>
&lt;p>No elaborate architecture, no self-made framework, no layer cake of abstractions, no complicated validation library.&lt;/p>
&lt;p>Just a simple for loop, a simple if, and raising an exception.&lt;/p>
&lt;p>In the past I believed I could encode everything in the type system. FP, it was called. But soon I was no longer solving the business problem. I was solving the type signature puzzle.&lt;/p>
&lt;p>As a junior like you, Bobby, I found his codebase too simple. But now that I have grown up, I appreciate his code more and more.&lt;/p>
&lt;p>So let&amp;rsquo;s try to run his tests. And here is the first thing I always loved about his projects: it is one command. &lt;code>pytest&lt;/code> runs the Python tests, and through a small plugin it also runs the JavaScript tests of the frontend. No README with twelve steps, no &amp;ldquo;oh, and for the frontend you also need to&amp;hellip;&amp;rdquo;. One command, and you know whether the whole thing works. It sounds like nothing, but I have rarely seen it since.&lt;/p>
&lt;p>Okay, we install the dependency tool and the libraries, and then we can run the tests:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">... &lt;span class="c1"># Okay, my attention span is running out,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">... &lt;span class="c1"># Bobby: man, this takes forever&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># okay, it&amp;#39;s done, now to run the tests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">pytest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test_invoice_totals.py V
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test_invoice_rounding.py V
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">starting the database
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">running the schema migration
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test_create_invoice.py V
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">shutting down database
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">frontend/test_payment_button.js V
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">starting the database
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">running the schema migration
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">test_payment_flow.js V
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">shutting down database
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I don&amp;rsquo;t know what you will do, Bobby, but I&amp;rsquo;m going to grab a coffee while this runs. I remember that it takes at least 15 minutes. But it is worth the wait.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======================&lt;/span> &lt;span class="m">0&lt;/span> failed, &lt;span class="m">300&lt;/span> passed in &lt;span class="nv">1023s&lt;/span> &lt;span class="o">========================&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Right, let me fix that one annoying thing in the UI first. The payment button is blue, but the corporate branding guide says buttons should be magenta. And the guide says it should say &amp;ldquo;Pay now&amp;rdquo; instead of &amp;ldquo;Create payment&amp;rdquo;.&lt;/p>
&lt;/blockquote>
&lt;p>Bobby, it&amp;rsquo;s not even our project.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: The branding guide applies to all projects. It says so on page one.&lt;/p>
&lt;/blockquote>
&lt;p>Of course it does. And because he is Bobby, he does it properly: a full test run before the change, the change, and another full run after it.&lt;/p>
&lt;p>&lt;em>17 minutes later&lt;/em>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======================&lt;/span> &lt;span class="m">7&lt;/span> failed, &lt;span class="m">293&lt;/span> passed in &lt;span class="nv">1023s&lt;/span> &lt;span class="o">========================&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Ah, it seems you got caught changing the behavior.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Okay, let&amp;rsquo;s update those tests.&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">freshDb&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">container&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">startPostgresContainer&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">migrate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">container&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">db&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">createDbClient&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">container&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">async&lt;/span> &lt;span class="nx">cleanup&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">stopPostgresContainer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">container&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">stopPostgresContainer&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">container&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">throw&lt;/span> &lt;span class="nx">error&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">it&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;initiates payment when the button is clicked&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">testDb&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">freshDb&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">invoice&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">createInvoice&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">testDb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">customerId&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">amount&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mf">125.50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">clickPayButton&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">invoice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">payment&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">testDb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">db&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;payments&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">where&lt;/span>&lt;span class="p">({&lt;/span> &lt;span class="nx">invoice_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">invoice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">payment&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveLength&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">payment&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">status&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;pending&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">payment&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">amount&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;125.50&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">auditLog&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">testDb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">db&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">select&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;audit_log&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">.&lt;/span>&lt;span class="nx">where&lt;/span>&lt;span class="p">({&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">entity_id&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">invoice&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">entity_type&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;payment&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">auditLog&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveLength&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">auditLog&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">].&lt;/span>&lt;span class="nx">action&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toBe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;created&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">button&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">screen&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getByRole&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;button&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">button&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveTextContent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Pay now&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">button&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">toHaveClass&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;brand-magenta&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">finally&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">testDb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">cleanup&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>&lt;em>Another 17 minutes later&lt;/em>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======================&lt;/span> &lt;span class="m">4&lt;/span> failed, &lt;span class="m">296&lt;/span> passed in &lt;span class="nv">1023s&lt;/span> &lt;span class="o">========================&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Bobby, you&amp;rsquo;d better limit yourself to only running the failing tests, otherwise we&amp;rsquo;ll be drinking coffee all day.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Fine.&lt;/p>
&lt;/blockquote>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="o">=======================&lt;/span> &lt;span class="m">4&lt;/span> failed in &lt;span class="nv">101s&lt;/span> &lt;span class="o">========================&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Ah, come on. Four tests and it still takes almost two minutes? Why does every test need its own database?&lt;/p>
&lt;/blockquote>
&lt;p>Because we don&amp;rsquo;t want tests interfering with each other.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Right. But why do we need a new PostgreSQL server for each one?&lt;/p>
&lt;/blockquote>
&lt;p>Good question. If we look at the profile, most of the tests don&amp;rsquo;t touch the database at all and finish in milliseconds. But about forty of them do, and every one of those first starts a fresh PostgreSQL container, which takes 20 seconds, and then runs the migrations, another 5 seconds. Forty times 25 seconds, and there go our 17 minutes.&lt;/p>
&lt;p>That can&amp;rsquo;t be how Mr. T.R. left it. I remember him being proud of how fast his tests were.
Maybe he left some kind of breadcrumb. Let&amp;rsquo;s look at the git history.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">commit c41e07 TheCathedralBuilder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;use testcontainers: start a fresh container per test to fix state issues...&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">commit 9a02cd Mr. T.R.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;reuse the database ...&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Of course. The cathedral builder. Mr. T.R. had already fixed it, and after he left, our favorite bureaucrat put a fresh container in every test, because things have to be the same everywhere, and every project in the company uses testcontainers now, by decree. The &amp;ldquo;state issues&amp;rdquo; in his commit message turned out to be one test that forgot to use its own database.&lt;/p>
&lt;p>So what did Mr. T.R. do? Ah nice, PostgreSQL allows us to generate a &amp;rsquo;template&amp;rsquo; database upfront.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">let&lt;/span> &lt;span class="nx">migrated&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kr">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">freshDb&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">testName&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="nx">migrated&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">createDatabaseTemplate&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">migrated&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">databaseName&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="sb">`test_&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">testName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">await&lt;/span> &lt;span class="nx">adminDb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">query&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb"> CREATE DATABASE &amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">databaseName&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb"> TEMPLATE test_template
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sb"> `&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">connect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">databaseName&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">it&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;initiates payment when the button is clicked&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="kr">async&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">db&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kr">await&lt;/span> &lt;span class="nx">freshDb&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;initiates_payment_when_button_clicked&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: What is this arcane magic, TEMPLATE??&lt;/p>
&lt;/blockquote>
&lt;p>It is a PostgreSQL feature that allows you to create a new database as a copy of another one, schema and all.
He started the database server once, ran the migration once, and then created a copy of the migrated database for each test.&lt;/p>
&lt;p>That is a clever use for it. Let me quickly try it out. Oh, it allowed us to create databases in less than 100ms.
So the tests were not slow by nature; it was the result of our favorite bureaucrat, who wanted things to be the same everywhere.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: I suppose this project was too fast in his opinion.&lt;/p>
&lt;/blockquote>
&lt;p>Now that I think of it, Mr. T.R. used to be able to tell quickly why a database test failed. Did you see the databases are not cleaned up? We can easily check what the state was in the failure cases.&lt;/p>
&lt;p>Bobby, I think we&amp;rsquo;d better revert our changes. Since we will waste several days to get them merged.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: But the branding&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>The branding will survive, Bobby. Better yet some cranky customers, wouldn&amp;rsquo;t be happy with a changed button.
We still have to continue our migration, but let&amp;rsquo;s note down what we learned:&lt;/p>
&lt;ul>
&lt;li>use a real database&lt;/li>
&lt;li>have explicit dependencies&lt;/li>
&lt;li>test the actual behavior&lt;/li>
&lt;li>avoid mocks&lt;/li>
&lt;/ul>
&lt;p>To make our database tests go fast we should:&lt;/p>
&lt;ul>
&lt;li>Reuse the database container.&lt;/li>
&lt;li>Run migrations only once.&lt;/li>
&lt;li>Create fresh databases from the template.&lt;/li>
&lt;li>Leave the test databases around so we can inspect failures.&lt;/li>
&lt;/ul>
&lt;p>That saves about 25 seconds per database test, so the whole suite drops from 17 minutes to about a minute. Having such a fast cycle time makes it so much easier to maintain a project.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Where did he learn this from?&lt;/p>
&lt;/blockquote>
&lt;p>I don&amp;rsquo;t know. But it seems he left another nugget before he left:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-md" data-lang="md">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## Testing strategy
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">The current integration tests are deliberately explicit.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">This makes failures easy to understand, but there are
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">too many assertions describing the exact shape of the
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">database and API responses.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Things to investigate:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> database templates instead of migrations per test V
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> snapshot testing for large response objects
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Ah, snapshot testing. That would simplify the codebase even more.
When was this committed?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">commit b712aa
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;farewell&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">commit 8f31a2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;document testing improvements&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">commit 9a02cd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;#34;reuse the database ...&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>And here the trail goes cold.&lt;/p>
&lt;h2 id="part-ii-the-foot-of-the-mountain">Part II: The Foot of the Mountain
&lt;/h2>&lt;p>So we went back to the valley, back to the tree and the tar.&lt;/p>
&lt;p>For a few days we tried to apply what we had learned. Use a real database, avoid the mocks, test the actual behavior. But every time we pulled one root out of the tar, two others got stuck. To test &lt;code>create_user&lt;/code> with a real database, we first had to untangle it from the user events, which were tangled with the billing, which was tangled with something nobody could explain to us.&lt;/p>
&lt;p>One evening I walked to the end of the valley, mostly to get away from the smell of tar, and looked up. Behind the mud, where the valley ended, there was a mountain. I had never noticed it before. Its slopes were steep, the top was lost in the clouds, and if the old stories were right, from up there you could see past the valley. Maybe even see what the valley was for.&lt;/p>
&lt;p>But we had no path, no map, and nobody to ask. Mr. T.R.&amp;rsquo;s trail ended at the foot of it.&lt;/p>
&lt;p>And then, out of a clear sky, like lightning striking the villain at the end of an old film, the corporate overlords did something right. Not that I noticed at the time.&lt;/p>
&lt;p>Ah, here we go again, the corporate overlords want to bring in another tool to make us go faster.
Here the board is making a new announcement.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Board member&lt;/strong>: We are proud to announce our deal to bring the latest and best AI models into our company.
AIchemy is the leading-edge model, and we predict that it will increase our productivity.
It can answer any question and is highly advanced in its knowledge.
We encourage you to try it out while the free credits last.&lt;/p>
&lt;/blockquote>
&lt;p>Yeah, &amp;ldquo;try it out&amp;rdquo;, more like we have to use it! All aboard the hype train, I guess.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Oh, let me install it right away.&lt;/p>
&lt;/blockquote>
&lt;p>It is a command line tool. When it starts, it greets you with its logo, a little glowing ring.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-Bash" data-lang="Bash">&lt;span class="line">&lt;span class="cl">&amp;gt; AIchemy prompt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-*&lt;span class="o">=&lt;/span>:&lt;span class="o">=&lt;/span>-:&lt;span class="o">=&lt;/span>::+..&lt;span class="o">=&lt;/span>:&lt;span class="o">=&lt;/span>::*:-*:&lt;span class="o">=&lt;/span>*&lt;span class="o">=&lt;/span>-*
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">=&lt;/span>%#:*+:*+-+-@@@+--*&lt;span class="o">=&lt;/span>*#-**::&lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:+-:+&lt;span class="o">=&lt;/span>:#+:#%@@@%&lt;span class="o">=&lt;/span>-#--+::*::-
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:&lt;span class="o">=&lt;/span>::-::+::%@@@@@@++::&lt;span class="o">=&lt;/span>::-:::
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:-:::::-@@%@@@@@@@@:::::::*#
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">+:.:::::::::@%@::::::::::#+&lt;span class="o">=&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#-.:::.-@@@@@+@@@@%::=::=*++&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:-.:+::%@@@@@@@@@%@#-&lt;span class="o">=&lt;/span>:-:.--
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">.&lt;span class="o">==&lt;/span>:*&lt;span class="o">=&lt;/span>:@@@@@@@@@@@@@+*:&lt;span class="o">==&lt;/span>.--
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:-&lt;span class="o">=&lt;/span>-#++@@@@@@@@@@@@@%&lt;span class="o">=&lt;/span>:&lt;span class="o">==&lt;/span>::-
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-#&lt;span class="o">=&lt;/span>:+:@@@@@@@@@@@@@@@-:+*--:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-&lt;span class="o">=&lt;/span>::&lt;span class="o">=&lt;/span>.-@@@@@@@@@@@@@::::&lt;span class="o">====&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">:::.:::@@@@@@@@@@@@@.::::+++
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">::#::::@@@%@@@@@@@@@:::*-:-.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">..*::::%@@@@@@@@@@@@:::&lt;span class="o">=&lt;/span>.%::
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">..--.-@@%@@@@@@@@@%@@.......
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">=&lt;/span>+-*@%@@@@@@@@@@@@@@@%%..-.:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">+ AIchemy, we have a legacy project at ~/projects/legacy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">We are inspired by ~/projects/mrtr-project and would like to improve the testing of our project.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">But how can we make this a swift improvement? We know the code is in a terrible state.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">We want to avoid a slow migration, so give us an answer as &lt;span class="k">if&lt;/span> you were a 10x engineer.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: Only a 10x engineer escapes the valley swiftly.
You are not a 10x engineer.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Rude.&lt;/p>
&lt;/blockquote>
&lt;p>Bobby wasn&amp;rsquo;t going to let that go. He leaned over my keyboard.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">+ Who is this 10x engineer? Tell us everything.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>:&lt;/p>
&lt;p>You ask how one might swiftly escape the valley.&lt;/p>
&lt;p>Be warned. There are many fates.&lt;/p>
&lt;p>Some engineers dwell in the Valley of Technical Debt until the end of their careers.&lt;/p>
&lt;p>Others escape the valley, only to build another one somewhere else.
They repeat the patterns they have never learned to escape.&lt;/p>
&lt;p>But a few climb above the valley.&lt;/p>
&lt;p>From there, they can see both the old land and the new.
They ask not only what the old system does, but what it was meant to do.
They learn the land. They study the roads others have walked.&lt;/p>
&lt;p>Then they build.&lt;/p>
&lt;p>And the new land must be easier to understand, easier to change, and better suited to the work than the old one.&lt;/p>
&lt;p>The old tales call such an engineer a 10x engineer.&lt;/p>
&lt;p>I know not the way.&lt;/p>
&lt;p>I have but read the old tales.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So where does this lead us?&lt;/p>
&lt;/blockquote>
&lt;p>Well, I suppose we&amp;rsquo;d best look at the commit logs, ask around and see how people use it?
But are we really going to sit in meeting after meeting? Since management wants AI, we could use it.
So here goes nothing:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">AIchemy, look at the project history and tell us what this system
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">is supposed to do.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">What was its original goal?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">What does it do today?
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">And are there simpler or alternative ways of solving the same problem?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>:
It is meant to be a CI system.
But it became an automation system.&lt;/p>
&lt;p>The engine is sprinkled throughout the codebase.
It can only be tested as a whole, not as parts.&lt;/p>
&lt;p>Its parts speak different languages
and have different expectations.&lt;/p>
&lt;p>There is tension.
Different views.
Different kingdoms.&lt;/p>
&lt;p>The parts think they are separate.&lt;/p>
&lt;p>They are not.
They are a single monolith wearing many uniforms.
If you seek to rebuild it, do not rebuild the kingdoms.&lt;/p>
&lt;p>Conquer the lands around them.&lt;/p>
&lt;p>Unify them under a single banner.&lt;/p>
&lt;p>Give each part clear borders.&lt;/p>
&lt;p>Then decide what deserves to remain.&lt;/p>
&lt;/blockquote>
&lt;p>That explains why we can&amp;rsquo;t really ask the other developers how things work: nobody sees the full picture anymore. Every team guards its own kingdom.&lt;/p>
&lt;p>I keep thinking about Mr. T.R.&amp;rsquo;s project. We have seen what a codebase can feel like when it is easy to work in, and it is nothing like the tree in the tar. I don&amp;rsquo;t think we can get there by patching the tree.&lt;/p>
&lt;p>Maybe we have to start fresh. Greenfield.&lt;/p>
&lt;p>I say it out loud and immediately feel unsure about it. Probably every engineer who ever built a second valley started by saying exactly that.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Isn&amp;rsquo;t that what it just warned us about? &amp;ldquo;Others escape the valley, only to build another one somewhere else.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Maybe, Bobby. Maybe that&amp;rsquo;s what we&amp;rsquo;ll end up doing. But Mr. T.R.&amp;rsquo;s project shows it doesn&amp;rsquo;t have to go that way. And I don&amp;rsquo;t see another way up.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So where do we start?&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: Take this ring, it will be your guidance.&lt;/p>
&lt;/blockquote>
&lt;p>Bobby points at the corner of the terminal, where the little ring icon is now pinned. I suppose that&amp;rsquo;s the ring. From now on, whenever we&amp;rsquo;re stuck, we can rub the ring. Well, type into it.&lt;/p>
&lt;p>I close the laptop.&lt;/p>
&lt;p>We need to understand what we&amp;rsquo;re actually trying to build.&lt;/p>
&lt;p>And how do we do that?&lt;/p>
&lt;p>One problem at a time.&lt;/p>
&lt;p>We start walking.&lt;/p>
&lt;h2 id="part-iii-the-ascent">Part III: The Ascent
&lt;/h2>&lt;p>Bobby, we will start from scratch. We pick the dependencies and technology.&lt;/p>
&lt;p>Otherwise we might accidentally break the existing system.&lt;/p>
&lt;p>We have to go fast. Faster than the existing system is expanding. Otherwise we will never catch up with it, let alone absorb the whole thing.&lt;/p>
&lt;p>Dear reader, a quick word before we start climbing. This is a long climb, and if I describe every handhold this post turns into a book and you walk away halfway up the mountain. So here I&amp;rsquo;ll stick to the path. Each stretch of the climb gets its own post later in this series, where we dig into the details.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Who are you talking to?&lt;/p>
&lt;/blockquote>
&lt;p>Nobody, Bobby. Let&amp;rsquo;s climb.&lt;/p>
&lt;p>The first wall we hit wasn&amp;rsquo;t even the tests. It was the layers not agreeing with each other.&lt;/p>
&lt;p>In the old system every layer had its own idea of what a build was. The database had a &lt;code>status&lt;/code> column, the backend had a &lt;code>BuildStatus&lt;/code> object with a slightly different set of values, the API returned its own JSON, and the frontend had a hand-written type that matched none of them. Bobby added a &lt;code>CANCELLED&lt;/code> status to the backend on his first day, and the frontend happily showed &amp;ldquo;undefined&amp;rdquo; for a week before anybody noticed. Somewhere in the old code someone had once added &lt;code>&amp;quot;SUCCEEDED&amp;quot;&lt;/code> next to &lt;code>&amp;quot;SUCCESS&amp;quot;&lt;/code>, because one layer said it one way and another layer said it the other way.&lt;/p>
&lt;p>We didn&amp;rsquo;t want to carry that up the mountain. So I rubbed the ring.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">+ AIchemy, how do we make the layers agree on the types to use?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: The light source casts shadows in one direction.
Do not mirror what is cast.
Let the projection fall where it lands.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: What does projection mean?&lt;/p>
&lt;/blockquote>
&lt;p>Good question. Like a projector in a cinema? Or a shadow on a wall?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: A shadow, I think. You can&amp;rsquo;t paint a shadow and expect the thing that casts it to change.&lt;/p>
&lt;/blockquote>
&lt;p>Huh. That&amp;rsquo;s it. We have been painting shadows. Every layer is a hand-painted copy of the layer next to it, so every change has to be painted twice, and the copies slowly drift apart. That&amp;rsquo;s the mirroring. If I change the query, I need to change the row record. If I change the row record, I need to change the API, and then the client.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So the frontend is the source! That&amp;rsquo;s what the user sees, that&amp;rsquo;s where the light falls.&lt;/p>
&lt;/blockquote>
&lt;p>No Bobby, that&amp;rsquo;s where the shadow lands. Although&amp;hellip; you have a point that the frontend is the part that matters to the user. It&amp;rsquo;s just not the part we should be painting by hand.&lt;/p>
&lt;p>What if we invert our design and start from the source? The database. We first write a query and its indexes, and then we know for sure what the return type is, and we can generate the database client from it. And the same trick works one layer up: some will first write an OpenAPI schema, but what if the server code itself is the schema? We generate the client from the server, and then it becomes very easy to find out whether the server and client disagree: the type checker tells us before we have run anything.&lt;/p>
&lt;p>And that&amp;rsquo;s the part that surprised me. A type checker is the cheapest test we have. It runs in a second, it doesn&amp;rsquo;t need a database, and it checks every caller, including the ones we would never have written a test for. A whole category of tests we no longer have to write.&lt;/p>
&lt;p>With the layers agreeing, we take Mr. T.R.&amp;rsquo;s database setup. Container started once, migrations run once, a fresh database from the template for every test. Now the database tests will not slow us down.&lt;/p>
&lt;p>Then Bobby adds an age and a last name to the user. The branding guide wants &amp;ldquo;Full name&amp;rdquo; on the profile page, of course. And a dozen tests break, because every one of them checks every property of a user, one by one.&lt;/p>
&lt;p>Why did we write our tests like this? Well, we ported them from Mr. T.R.&amp;rsquo;s project, and Mr. T.R. was a bit paranoid. Let&amp;rsquo;s look at one of them:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">setup_user_db&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dike&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jimmy&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">delete_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># But here comes the kicker:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># what if delete_user drops all users,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># or for some reason mangles jimmy and renames him to jimmy hendrix?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># so Mr. T.R. added this to prove that no other change was made by the query:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dike&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jimmy&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>I think I understand where the paranoia came from. His project did too many things. Invoices, payments, users, audit logs, all in one place. When a system does too many things, any change can touch anything, so you end up checking everything, every time.&lt;/p>
&lt;p>What was on his note again? &amp;ldquo;Snapshot testing for large response objects.&amp;rdquo; Let&amp;rsquo;s rub the ring, hopefully it gives us some guidance on how to make this easier.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: The snapshot runs the code and freezes what was once valid.&lt;/p>
&lt;/blockquote>
&lt;p>So we run the code, write the result to the test file, and keep it as the expected output of the test.
Oh, this is handy. Remember that API call that had to match a format exactly, but we had no typed schema?
Or right now, with the age and the last name, where we would have to update a dozen tests by hand?&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">setup_user_db&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">snapshot&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">delete_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">snapshot&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>After running and accepting the updated results:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">setup_user_db&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">snapshot&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dike&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jimmy&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">delete_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">assert&lt;/span> &lt;span class="n">get_users&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">snapshot&lt;/span>&lt;span class="p">([&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;dike&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">User&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;jimmy&amp;#34;&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>So now we see that the delete operation has no effect on the other users! And when we add a field, we rerun, read the diff, and accept it, instead of editing a dozen tests by hand.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: But how do we know it doesn&amp;rsquo;t influence other tables?&lt;/p>
&lt;/blockquote>
&lt;p>Hmm, fair. It doesn&amp;rsquo;t tell us that.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: We could snapshot the whole database before and after, and assert that only the users table changed.&lt;/p>
&lt;/blockquote>
&lt;p>Okay, that&amp;rsquo;s doable.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: But then the application could still mangle something in memory, a cache or so. So we&amp;rsquo;d have to snapshot the memory as well. And the files it writes. And since we can never snapshot everything, we&amp;rsquo;d have to fuzz the inputs to find what we missed. And what if Tommy hid a &lt;code>date()&lt;/code> in there that only renames Jimmy to Jimmy Hendrix on Tuesdays, or Brad to Brad Pitt? Then we need a linter that bans date() calls, and then there is random()&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>Bobby. Stop.&lt;/p>
&lt;p>Every step you just described costs more than the one before it and catches less. It is a wall that gets steeper the closer you get to it, a vertical asymptote. You never reach &amp;ldquo;total&amp;rdquo;, you just keep spending.&lt;/p>
&lt;p>Oh snap, now we know where Mr. T.R.&amp;rsquo;s paranoia came from: the pursuit of total testing. In a perfect test environment all the side effects would be tracked, but we don&amp;rsquo;t live in one, and neither did he. So we stop somewhere. The code does what we expect, and the neighbors we can see in the snapshot are untouched. Bobby&amp;rsquo;s other tables stay a blind spot, and we write it down as one, so at least we know where it is.&lt;/p>
&lt;p>Now the harder part. In our system, deleting a user also has to reach the billing system, and the billing system, being a telecom billing system, times out whenever it feels like it. So we retry: wait 10 seconds, then 20, then 40, and then we give up.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">delete_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">billing&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">name&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">delete_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="n">delay&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">40&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">billing&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">close_account&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="n">BillingTimeout&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">await&lt;/span> &lt;span class="n">asyncio&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">sleep&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">delay&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">raise&lt;/span> &lt;span class="n">BillingUnavailable&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>If we test this as is, the test for the failure path sleeps 10 + 20 + 40 seconds. More than a minute, just to test this one failure path. And there will be more paths like it.&lt;/p>
&lt;p>So how would we best test this?&lt;/p>
&lt;p>My first thought is that we should drop the retry from the test. But then we push the retrying onto other parts of the system, and we are no longer testing the thing we actually run in production.&lt;/p>
&lt;p>Let&amp;rsquo;s ask AIchemy for advice.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: He who commands time does not wait.&lt;/p>
&lt;/blockquote>
&lt;p>Anything else you&amp;rsquo;d like to add?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: I have spoken.&lt;/p>
&lt;/blockquote>
&lt;p>Well, so far so good. Let&amp;rsquo;s have a look at how others solve this.&lt;/p>
&lt;p>Ah, Mr. T.R. solved it by mocking sleep. That works, but then there is the problem that other pieces of code check the time, and no real 10s have passed. So now we would have to mock the clock as well.&lt;/p>
&lt;p>Let us have a look at how other languages solve this. Here is a little gem: Go&amp;rsquo;s &lt;code>testing/synctest&lt;/code>.&lt;/p>
&lt;p>The important part is that inside a synctest bubble the clock is fake: &lt;code>time.Sleep()&lt;/code> doesn&amp;rsquo;t actually wait, time jumps forward as soon as every goroutine in the bubble is blocked, and &lt;code>synctest.Wait()&lt;/code> waits until all those goroutines are durably blocked. So instead of actually waiting, the test can move time forward.&lt;/p>
&lt;p>But can we do this in Python? It seems possible if we are using asyncio everywhere since then all code runs in a coroutine. But building something like that ourselves would take weeks, and we don&amp;rsquo;t have weeks.&lt;/p>
&lt;p>So I suggest we delegate it to our new hire, Claude&amp;hellip;&lt;/p>
&lt;p>And then I hesitate. Wasn&amp;rsquo;t I making fun of Tommy and his AI-assisted commit a few weeks ago? I think there is a difference, and I hope it&amp;rsquo;s a real one. Tommy asked the AI to write his tests, so it wrote tests that test nothing. We ask it to build us a tool, a clock we can control, and then we write the tests ourselves, and we read what it built.&lt;/p>
&lt;p>It takes an afternoon. And the trick turns out to be small enough to explain over a coffee. A sleeping task doesn&amp;rsquo;t start a real timer; it parks itself on a future and writes its wake-up time on a heap. The clock only moves when nothing in the event loop is runnable anymore and no query is still on its way to the database. Then it jumps straight to the earliest wake-up time and lets those tasks run. To know that nothing is in flight it peeks into the event loop&amp;rsquo;s selector, which is a bit naughty, but it works.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_delete_user_gives_up_on_billing&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">billing&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">clock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FakeClock&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">billing&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">always_time_out&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">clock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">patch_asyncio_sleep&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">task&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">asyncio&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">create_task&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">delete_user&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">db&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">billing&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;jane&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">async&lt;/span> &lt;span class="k">for&lt;/span> &lt;span class="n">attempt&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">clock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">eventually&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">within_s&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">120&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">attempt&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">task&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">done&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="nb">isinstance&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">task&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">exception&lt;/span>&lt;span class="p">(),&lt;/span> &lt;span class="n">BillingUnavailable&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">clock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">now&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="mi">70&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Seventy seconds of retrying, in a few milliseconds. Honestly, I think this clock is one of the best tools we have built so far, and before AI I would never have found the time to build it.&lt;/p>
&lt;p>Now our test scope increases. We don&amp;rsquo;t just control the code anymore, we also control the time within our application.&lt;/p>
&lt;p>But then we hit another problem.&lt;/p>
&lt;p>The database doesn&amp;rsquo;t agree with us.&lt;/p>
&lt;p>We can move the clock forward inside our application, but when we ask the database what time it is, the database still thinks it is now.&lt;/p>
&lt;p>Let us read the message from AIchemy again.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: He who commands time does not wait.&lt;/p>
&lt;/blockquote>
&lt;p>Maybe we can command the time we give to the database as well.&lt;/p>
&lt;p>We could make the timestamp an optional argument to the query. In production we don&amp;rsquo;t provide it, so the database uses its own time. In tests we provide the timestamp, and now the test can decide what time the query sees.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: But what if the time between the database and our system doesn&amp;rsquo;t match?&lt;/p>
&lt;/blockquote>
&lt;p>Oh Bobby, don&amp;rsquo;t you want to be done with the migration? We have NTP for this case, the paranoid can add an alert for when the time drifts.&lt;/p>
&lt;p>We could probably make the database clock and application clock agree somehow. Or we could give each test its own database with its own notion of time. I&amp;rsquo;m not sure how we&amp;rsquo;d do either of those things, and both sound like another abstraction, another thing to maintain, and another migration.&lt;/p>
&lt;p>Or we could just pass the timestamp to the query and accept the blind spot.&lt;/p>
&lt;p>We don&amp;rsquo;t need to control every clock in the system to test the behavior we care about. We need to control the clock where it affects the behavior we are testing.&lt;/p>
&lt;p>So the database keeps its real clock in production, but the test can explicitly provide a timestamp when it needs to.&lt;/p>
&lt;p>Another dependency has moved out of the loop.&lt;/p>
&lt;p>A week later Bobby spends an afternoon on a bug where the build page shows the oldest build as the latest one. The query used to return the builds sorted by date, someone dropped the &lt;code>ORDER BY&lt;/code> because &amp;ldquo;the index already sorts it&amp;rdquo;, and on a small table that was even true. The code below it simply took the first row. None of our tests caught it, because with three builds in a test database the order happens to be right.&lt;/p>
&lt;p>That kind of thing is hard to test. But it is very easy to check while the code runs:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="n">builds&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">db&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">recent_builds&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">repo&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="n">builds&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">builds&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">previous&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">builds&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">builds&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">builds&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">:]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">previous&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">created_at&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">build&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="p">,&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;recent_builds must return newest first&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Whenever we make an assumption in the code, like &amp;ldquo;this query returns the data sorted by column X&amp;rdquo;, we can pin it down with an assertion. It&amp;rsquo;s cheap, but it makes the program fail when the assumption is violated. It fails fast, instead of quietly doing the wrong thing and leaving us with a long investigation into why.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So we just crash in production?&lt;/p>
&lt;/blockquote>
&lt;p>Yes. Loudly, instead of quietly showing the wrong build for a month.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Maybe the users would prefer the wrong build over an error page.&lt;/p>
&lt;/blockquote>
&lt;p>Maybe. I&amp;rsquo;m honestly not sure. But I&amp;rsquo;d rather know.&lt;/p>
&lt;p>Somewhere around here I looked back down the slope, and I noticed what we had been doing all this time without ever deciding to do it. The template database, the generated types, the snapshots, the clock, the assertions. Every single one of them made the time between changing something and knowing whether it worked shorter. Nobody had told us that was the goal. We just kept removing whatever made us wait.&lt;/p>
&lt;p>But our deployment currently is, we run the gitlab ci, publish an docker image and a helm chart, we install this through gitops. Wait for the pods to come up. Submit a test build, and wait for that build to finish. The first time we did it, the pipeline took forty minutes. And the first time we did it, it failed at the very end, because one of the labels we generated for the pods contained a timestamp, and Kubernetes doesn&amp;rsquo;t allow colons in a label value. Forty minutes to learn that a string had a colon in it.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: We can cache it!&lt;/p>
&lt;/blockquote>
&lt;p>And he did. Docker layer caching in GitLab CI, the dependency cache kept between runs, the jobs running in parallel instead of one after the other. Forty minutes became fifteen. Bobby was very proud, and to be fair, he had every right to be.&lt;/p>
&lt;p>But fifteen minutes is still a coffee. And the next time something failed at the end of the pipeline, I rubbed the ring.&lt;/p>
&lt;p>Make the loop faster. I suppose that is what everybody means when they say &amp;ldquo;shift left&amp;rdquo;. But how far can we shift left?&lt;/p>
&lt;p>Let&amp;rsquo;s ask AIchemy.&lt;/p>
&lt;blockquote>
&lt;p>AIchemy: Left and right are relative.&lt;/p>
&lt;/blockquote>
&lt;p>Bobby: Relative to what?
AIchemy: To where you stand.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>BOBBY&lt;/strong>: But i stand in the middle?&lt;/p>
&lt;/blockquote>
&lt;p>Perhaps, you stand in the middle. Me too. Aren&amp;rsquo;t we all the center of everything?&lt;/p>
&lt;p>Anyway lets try this once more.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">+ AIchemy, our deploy pipeline takes fifteen minutes. How do we make it faster?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: The swiftest pilgrim is the one who did not need to walk.&lt;/p>
&lt;/blockquote>
&lt;p>My first reading was: skip steps. Build only the image that changed, skip the test build when only the docs changed. That helps a bit. But the clue kept bugging me. A pipeline that doesn&amp;rsquo;t run is faster than any pipeline we could cache.&lt;/p>
&lt;p>Why were we running that pipeline at all? Most of the times we pushed, we weren&amp;rsquo;t deploying to production. We were experimenting. We wanted to know whether our code worked inside the cluster, with the real network, the real permissions, the real Kubernetes. And for that, we were baking images, pushing them, and rolling them out, every single time, as if each experiment were a release.&lt;/p>
&lt;p>So we split the two. For experimenting, we stopped building images altogether: a development pod runs in the dev cluster, wearing the same labels and permissions as the real service, and our code is synced into it every time we save a file. Seconds instead of fifteen minutes. Production keeps the full, slow, cached pipeline, and we make it boring on purpose. The 10x part happens before production; production itself should be the dullest part of the week. How that development pod works is a story for one of the follow-up posts.&lt;/p>
&lt;p>With go it would be even easier&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">watchexec --restart -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> sh -c &lt;span class="s1">&amp;#39;go build -o ./app . &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> kubectl cp ./app dev-pod:/tmp/app &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> kubectl exec dev-pod -- /tmp/app&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>But boring takes more than a slow pipeline.&lt;/p>
&lt;p>A week later the firmware team&amp;rsquo;s nightly build failed. It had run for an hour, compiled everything, run all their tests, and then, at the very end, when it tried to upload the artifacts, the upload token turned out to be expired. An hour of compute and an hour of waiting, to learn something we could have known in the first second.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: We could retry the upload.&lt;/p>
&lt;/blockquote>
&lt;p>Retry an expired token, Bobby?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: &amp;hellip;Okay, no.&lt;/p>
&lt;/blockquote>
&lt;p>But it made me look at our own services, and they were no better. Our runner happily started without the registry credentials and only complained the first time it needed them. If an environment variable was missing, we found out when that one code path ran, sometimes days after the deploy.&lt;/p>
&lt;p>So we turned it around. At boot, the services check everything they need: the environment variables, the database connection, the tokens. If anything is missing or invalid, they refuse to start. Kubernetes then never sends traffic to the new pods, the rollout stalls, and the old version keeps running. Rolling back becomes not rolling forward.&lt;/p>
&lt;p>And since we are the CI system, we can do the same for the builds. An action checks its token before it starts the hour of compiling, and refuses to run if the token is invalid. Tokens can also expire halfway through, so the check is part of the action&amp;rsquo;s health check, and a build stops in the middle instead of finishing a calculation that nobody can upload.&lt;/p>
&lt;p>Better a deployment that refuses to boot than one that boots and falls over at 3 a.m.&lt;/p>
&lt;p>Then the firmware team asked us for a new check. From version 1.5 on, every build should produce an SBOM. Fair enough. I opened the code that decides which checks to run, and found this:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">required_checks&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">repo&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">version&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;lint&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;unit&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">repo&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;firmware&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;1.1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="n">version&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;1.4&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checks&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;legacy-signing&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="n">repo&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;firmware&amp;#34;&lt;/span> &lt;span class="ow">and&lt;/span> &lt;span class="n">version&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="n">Version&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;1.5&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">checks&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">append&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;secure-boot&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">checks&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>We had ported it from the old system without thinking, and it had already grown. Every rule about their versions lived in our code, so every new rule meant a change to our system, a test of our system, and a deploy of our system. For a rule that is really about their repository.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: I&amp;rsquo;ll just add another &lt;code>if&lt;/code>.&lt;/p>
&lt;/blockquote>
&lt;p>Wait.&lt;/p>
&lt;p>What if that knowledge moves to where it belongs? The firmware repo knows which checks it needs; it&amp;rsquo;s their code. So each repo carries a small file with the checks it needs, and our system asks the code: which checks do you need? An added bonus is that if a check gets updated, it can only be merged if the new check passes. You can&amp;rsquo;t update the CI and break the target repo anymore. And the version logic simply disappears, because the version is the commit. Branch 1.4 carries its own file, branch 1.5 carries its own.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So we just pushed the work onto them.&lt;/p>
&lt;/blockquote>
&lt;p>Partly, yes. But they were already doing the work. Every time they needed a check, they filed a ticket with us and waited for us to deploy. Now they change a file in their own repo. I&amp;rsquo;m not sure every check belongs there, but these ones did.&lt;/p>
&lt;p>And then something odd happened. We deleted &lt;code>required_checks&lt;/code>, and its tests, and all the deploys it would have needed. Not faster. Gone.&lt;/p>
&lt;p>The fastest loop we had found so far was one that never needed to run.&lt;/p>
&lt;p>I didn&amp;rsquo;t know what to do with that thought. Every trick on the way up had been about making the work faster. This one made the work unnecessary. And if the best way to speed up some work is to not have to do it, then how much of what happens down in the valley needs doing at all?&lt;/p>
&lt;p>I rubbed the ring one more time.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>AIchemy&lt;/strong>: You have climbed high enough to ask. Go and look.&lt;/p>
&lt;/blockquote>
&lt;p>There was nothing left on the slope that we wanted to make faster. So we kept walking up.&lt;/p>
&lt;h2 id="part-iv-the-summit">Part IV: The Summit
&lt;/h2>&lt;p>We finally reached the top. But something was off. Normally a mountain is made of rock, but this one was made of dirt.
The summit also seemed rather large for a summit.&lt;/p>
&lt;p>There was a man sitting there in a suit, a bit worn at the elbows, looking out over the valley. Not the kind of suit you wear to impress anyone. The kind you start wearing when you stop writing code and start attending meetings about it.
He turned when he heard us.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: You made it.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Are you the 10x engineer?&lt;/p>
&lt;/blockquote>
&lt;p>He laughed, as if it was foolish of us to still believe in such a myth.
He pointed down into the valley.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: This valley is something I started. I built part of it.&lt;/p>
&lt;/blockquote>
&lt;p>We looked down and saw a lot of people with shovels, all busy doing something. Some were throwing their dirt into other people&amp;rsquo;s holes, while others were moving it from one place to another and then back again. Somewhere in between, I could make out the old man with the gray beard, running from team to team, begging people to stop touching things.&lt;/p>
&lt;p>There seemed to be a lot of work going on, but somehow everyone was slowing everyone else down.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: That section over there? Mine.&lt;/p>
&lt;/blockquote>
&lt;p>Bobby looked at the enormous hole.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: You dug that?&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: Not all of it.&lt;/p>
&lt;/blockquote>
&lt;p>The man pointed again.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: That part was already there. I made it deeper.&lt;/p>
&lt;/blockquote>
&lt;p>We stood beside him for a while, looking down at the valley.&lt;/p>
&lt;p>Far away, on the other side, I noticed someone else sitting on the edge, in a folding chair, looking at the same hole from a different angle.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: That&amp;rsquo;s our CTO. From his side it looks like a mountain range. He&amp;rsquo;s quite proud of it.&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: Oh, and you really came on the right day. My request finally came through.&lt;/p>
&lt;/blockquote>
&lt;p>We saw a group of middle managers bringing in excavators.&lt;/p>
&lt;p>Bobby, with your young eyes, can you make out the brands of those things?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Anthropic, ChatGPT.&lt;/p>
&lt;/blockquote>
&lt;p>And the one that seems to be burying itself in the soil?&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Grok&amp;hellip;&lt;/p>
&lt;/blockquote>
&lt;p>Bobby looked at the man.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Why don&amp;rsquo;t you come with us?&lt;/p>
&lt;/blockquote>
&lt;p>The man smiled.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: Come where?&lt;/p>
&lt;/blockquote>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Out of here.&lt;/p>
&lt;/blockquote>
&lt;p>He looked back at the valley.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: I like the view.&lt;/p>
&lt;/blockquote>
&lt;p>We looked down again.&lt;/p>
&lt;p>From up there, the whole thing almost made sense. Every layer, every workaround, every compatibility requirement, every abstraction, and every team that had dug a little deeper while believing they were building a little higher.
The man had once been one of the engineers who knew enough to see how the whole thing fitted together.
Then he had become a manager.&lt;/p>
&lt;p>Now he mostly watched.&lt;/p>
&lt;p>&amp;ldquo;Doesn&amp;rsquo;t it bother you?&amp;rdquo; I asked.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: Of course.&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;Then why stay?&amp;rdquo;&lt;/p>
&lt;p>He shrugged.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>The man&lt;/strong>: Someone has to watch the hole.&lt;/p>
&lt;/blockquote>
&lt;p>Bobby looked at me, and I looked back at the man. I thought about all the things we&amp;rsquo;d discovered on the way up, all the problems we&amp;rsquo;d managed to solve, and all the things we could probably still improve if we stayed.&lt;/p>
&lt;p>&amp;ldquo;Come on, Bobby.&amp;rdquo;&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: Where are we going?&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;I don&amp;rsquo;t know yet. Let&amp;rsquo;s look around.&amp;rdquo;&lt;/p>
&lt;p>We left him on his rock and walked across the summit. Now that we were up there, it was strangely flat, and strangely large. No rocks, no peak, packed dirt with the tracks of the excavators running through it.&lt;/p>
&lt;p>At the far edge, where the excavators hadn&amp;rsquo;t been, a narrow path led away. Next to it stood a small pile of stones, stacked carefully, the way hikers mark a trail. Wedged between the top stones was a faded sticky note.&lt;/p>
&lt;p>&lt;em>farewell — T.R.&lt;/em>&lt;/p>
&lt;p>Bobby, I think we found a way out of here.&lt;/p>
&lt;p>We followed the narrow path away from the summit.&lt;/p>
&lt;p>Somewhere in the valley, AI was generating code. They were digging faster now.&lt;/p>
&lt;p>After a while, I looked back.&lt;/p>
&lt;p>From here, the shape of the landscape was different. What had looked like a mountain from below was now a vast depression in the earth, surrounded by land stretching out towards the horizon.&lt;/p>
&lt;p>I stopped.&lt;/p>
&lt;p>&amp;ldquo;Bobby, it was never a mountain.&amp;rdquo;&lt;/p>
&lt;p>He looked back.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: What do you mean?&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;It was a quarry. We&amp;rsquo;ve been climbing out of a hole.&amp;rdquo;&lt;/p>
&lt;p>Bobby looked towards the world beyond it.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So there&amp;rsquo;s a whole world out there?&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;Looks like it.&amp;rdquo;&lt;/p>
&lt;p>He was quiet for a moment.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Bobby&lt;/strong>: So perhaps the 10x engineer is somewhere out there.&lt;/p>
&lt;/blockquote>
&lt;p>&amp;ldquo;Perhaps.&amp;rdquo;&lt;/p>
&lt;p>We continued walking.&lt;/p></description></item></channel></rss>