<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Testing on Nikhil Adhikari</title><link>https://kai2055.github.io/tags/testing/</link><description>Recent content in Testing on Nikhil Adhikari</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><lastBuildDate>Fri, 24 Jul 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://kai2055.github.io/tags/testing/index.xml" rel="self" type="application/rss+xml"/><item><title>The Test Suite That Was Too Slow To Run</title><link>https://kai2055.github.io/p/test-suite-too-slow/</link><pubDate>Fri, 24 Jul 2026 00:00:00 +0200</pubDate><guid>https://kai2055.github.io/p/test-suite-too-slow/</guid><description>
 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;The short version, for anyone:&lt;/strong&gt; A project&amp;rsquo;s automated tests are its safety
net — but only if they get run. This suite took over 15 minutes, which in
practice means developers skip it, which means the safety net isn&amp;rsquo;t there. Every
test was correct; the problem was speed. This is the story of cutting it from
&lt;strong&gt;15m 46s to 8.5 seconds&lt;/strong&gt; — not by removing tests, but by noticing the same
expensive work was being redone dozens of times. The theme: &lt;em&gt;correct is not the
same as usable.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Full technical walkthrough below.&lt;/em&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Fast suite 15m 46s → 8.5s · &lt;strong&gt;Also found:&lt;/strong&gt; an 82-vs-83 chunk
discrepancy open since early July.&lt;/p&gt;
&lt;h2 id="framing-this-was-not-a-bug"&gt;Framing: this was not a bug
&lt;/h2&gt;&lt;p&gt;Every test passed. Every test was correct. Nothing produced a wrong answer. The
defect was that the suite took &lt;strong&gt;15 minutes and 46 seconds&lt;/strong&gt;, which in practice
means you stop running it. A test suite you avoid running provides no safety at all,
however correct it is.&lt;/p&gt;
&lt;p&gt;So this is a performance defect in the test harness, not a bug in the system —
worth being precise about, because the fix is different in kind. Nothing was
repaired. Work that was being done repeatedly was made to happen once.&lt;/p&gt;
&lt;h2 id="the-symptom"&gt;The symptom
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;85 passed, 1 deselected in 946.89s (0:15:46)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The project&amp;rsquo;s stated test architecture is pure-core / impure-shell: pure logic
tested in milliseconds, slow integration tests run deliberately. The 29 agent tests
already demonstrated this — fully mocked, 0.19 seconds. So the architecture was
right and the suite still took a quarter of an hour. Something was not matching the
design.&lt;/p&gt;
&lt;h2 id="measuring-instead-of-guessing"&gt;Measuring instead of guessing
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytest tests/ -m &lt;span style="color:#e6db74"&gt;&amp;#34;not integration&amp;#34;&lt;/span&gt; --durations&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--durations&lt;/code&gt; prints the slowest tests. The output split cleanly into two groups,
and the distinction between them turned out to be the whole story.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Group 1 — time in &lt;code&gt;setup&lt;/code&gt;:&lt;/strong&gt; four tests, ~90–110 seconds each, all in setup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Group 2 — time in &lt;code&gt;call&lt;/code&gt;:&lt;/strong&gt; four tests, ~105–217 seconds each, in the test body.&lt;/p&gt;
&lt;p&gt;And once setup was done, the tests themselves ran in &lt;strong&gt;under two seconds&lt;/strong&gt;. The
tests were never slow. The setup was.&lt;/p&gt;
&lt;h2 id="cause-1-a-fixture-doing-the-same-work-eight-times"&gt;Cause 1: a fixture doing the same work eight times
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@pytest.fixture&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;indexed_chunks&lt;/span&gt;(temp_chroma, corpus_path, expected_chunks):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; docs &lt;span style="color:#f92672"&gt;=&lt;/span&gt; load_documents(corpus_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunks &lt;span style="color:#f92672"&gt;=&lt;/span&gt; chunk_documents(docs)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; index_chunks(chunks)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; chunks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No &lt;code&gt;scope&lt;/code&gt; argument, so pytest defaults to function scope: the fixture runs fresh
for every test that requests it. Embedding 82 chunks through nomic-embed-text on CPU
costs roughly 100 seconds. Four tests used the fixture. Four identical embedding
passes, ~400 seconds, for one corpus that never changed between them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix — session scope&lt;/strong&gt;, so the corpus is embedded once and shared. One
complication: &lt;code&gt;temp_chroma&lt;/code&gt; used pytest&amp;rsquo;s function-scoped &lt;code&gt;monkeypatch&lt;/code&gt;, which a
session-scoped fixture can&amp;rsquo;t consume, so the replacement instantiates
&lt;code&gt;pytest.MonkeyPatch()&lt;/code&gt; directly and undoes it manually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff, stated plainly:&lt;/strong&gt; function scope exists for isolation — every test
gets a clean store. Session scope trades that away for speed. Acceptable here
because these tests read from the store rather than corrupting it — and because a
suite nobody runs is worse than a small isolation risk. Result: 15m 46s → 11m 25s.&lt;/p&gt;
&lt;h2 id="cause-2-a-test-embedding-82-chunks-to-count-to-82"&gt;Cause 2: a test embedding 82 chunks to count to 82
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_vector_count_matches_chunk_count&lt;/span&gt;(corpus_path, expected_chunks):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; docs &lt;span style="color:#f92672"&gt;=&lt;/span&gt; load_documents(corpus_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunks &lt;span style="color:#f92672"&gt;=&lt;/span&gt; chunk_documents(docs)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunks_with_vectors &lt;span style="color:#f92672"&gt;=&lt;/span&gt; embed_chunks(chunks)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; len(chunks_with_vectors) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; expected_chunks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;110 seconds. Read the assertion: it checks that &lt;code&gt;embed_chunks&lt;/code&gt; returns one vector
per chunk it was given. It never inspects a single vector. That property does not
depend on corpus size — five chunks prove it as well as 82. Slicing to &lt;code&gt;chunks[:5]&lt;/code&gt;
took it from &lt;strong&gt;110s → 4.67s&lt;/strong&gt;, and the assertion got &lt;em&gt;better&lt;/em&gt;: the real property is
&amp;ldquo;same number out as in,&amp;rdquo; not &amp;ldquo;82.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="cause-3-genuinely-slow-tests-were-not-labelled"&gt;Cause 3: genuinely slow tests were not labelled
&lt;/h2&gt;&lt;p&gt;Three tests could not be shrunk — &lt;code&gt;test_store_and_search&lt;/code&gt;, &lt;code&gt;test_search_with_filter&lt;/code&gt;,
&lt;code&gt;test_idempotency&lt;/code&gt; — because they genuinely need a fully populated store; that&amp;rsquo;s the
thing under test. They needed a &lt;em&gt;label&lt;/em&gt;, not an optimisation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[pytest]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;markers&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; slow: embeds the corpus, needs Ollama
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; integration: full end-to-end run across components&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three speeds instead of two:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytest -m &lt;span style="color:#e6db74"&gt;&amp;#34;not slow and not integration&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 8.5s - run constantly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytest -m &lt;span style="color:#e6db74"&gt;&amp;#34;slow&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# ~5 min - before committing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pytest -m &lt;span style="color:#e6db74"&gt;&amp;#34;integration&amp;#34;&lt;/span&gt; &lt;span style="color:#75715e"&gt;# ~100 min - deliberately&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The middle tier is the one that was missing. Previously &amp;ldquo;not integration&amp;rdquo; meant 15
minutes, so the only real choice was 15 minutes or nothing.&lt;/p&gt;
&lt;h2 id="result"&gt;Result
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Stage&lt;/th&gt;
 &lt;th&gt;Time&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Start&lt;/td&gt;
 &lt;td&gt;15m 46s&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;After session-scoped fixtures&lt;/td&gt;
 &lt;td&gt;11m 25s&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;After rewriting the count test&lt;/td&gt;
 &lt;td&gt;~9m&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;After marking slow tests&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;8.5s&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;78 tests run by default. 8 parked behind markers.&lt;/p&gt;
&lt;h2 id="the-side-finding-82-vs-83"&gt;The side finding: 82 vs 83
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;conftest.py&lt;/code&gt; expects 82 chunks and the chunking test passes — so chunking really
does produce 82. But the live store at &lt;code&gt;data/chromadb&lt;/code&gt; held &lt;strong&gt;83&lt;/strong&gt;. This
discrepancy had been open since early July. It&amp;rsquo;s now explained: the corpus is
correct, and the live store contained one orphan chunk from an earlier ingest — a
document later changed or removed without the store being rebuilt. A clean re-index
resolves it, worth doing before further calibration, since a stale chunk can surface
in retrieval and quietly distort a measurement.&lt;/p&gt;
&lt;h2 id="what-this-is-a-story-about"&gt;What this is a story about
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Correct is not the same as usable.&lt;/strong&gt; Every test passed and asserted something
true. The suite still failed at its actual job — being run often enough to catch
regressions early. &lt;strong&gt;Measure before optimising:&lt;/strong&gt; the &lt;code&gt;setup&lt;/code&gt;-vs-&lt;code&gt;call&lt;/code&gt; split
pointed straight at two different causes needing two different fixes. &lt;strong&gt;Ask what a
test is actually asserting:&lt;/strong&gt; the 110-second count test verified a property that had
nothing to do with corpus size. &lt;strong&gt;And some slowness is real&lt;/strong&gt; — three tests genuinely
need a populated store, so the fix was a marker, letting them run deliberately
rather than be skipped by accident.&lt;/p&gt;</description></item></channel></rss>