<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>vitest on Nikola Milovic</title>
    <link>https://nikolamilovic.com/tags/vitest/</link>
    <description>Recent content in vitest on Nikola Milovic</description>
    <generator>Hugo -- 0.118.2</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 04 May 2025 10:43:38 +0200</lastBuildDate>
    <atom:link href="https://nikolamilovic.com/tags/vitest/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fun &amp; Sane Node.js TDD: Supercharge Postgres Tests with PGLite, Drizzle &amp; Vitest</title>
      <link>https://nikolamilovic.com/posts/fun-sane-node-tdd-postgres-pglite-drizzle-vitest/</link>
      <pubDate>Sun, 04 May 2025 10:43:38 +0200</pubDate>
      <guid>https://nikolamilovic.com/posts/fun-sane-node-tdd-postgres-pglite-drizzle-vitest/</guid>
      <description>Fun &amp;amp; Sane Node.js TDD: Supercharge Postgres Tests with PGLite, Drizzle &amp;amp; Vitest Hey everyone! Let&amp;rsquo;s talk about testing in Node.js, specifically when databases get involved.
Unit tests? Absolutely essential for checking your functions in isolation. But let&amp;rsquo;s be real, at some point, you need to know if your code actually plays nice with a real database, message queue, or whatever external service it relies on. Mocking everything can feel like building a house of cards, fragile, prone to hiding nasty bugs that only surface when things interact for real.</description>
      <content:encoded><![CDATA[<h1 id="fun--sane-nodejs-tdd-supercharge-postgres-tests-with-pglite-drizzle--vitest">Fun &amp; Sane Node.js TDD: Supercharge Postgres Tests with PGLite, Drizzle &amp; Vitest</h1>
<p>Hey everyone! Let&rsquo;s talk about testing in Node.js, specifically when databases get involved.</p>
<p>Unit tests? Absolutely essential for checking your functions in isolation. But let&rsquo;s be real, at some point, you <em>need</em> to know if your code actually plays nice with a real database, message queue, or whatever external service it relies on. Mocking everything can feel like building a house of cards, fragile, prone to hiding nasty bugs that only surface when things interact for real. And shared dev databases? Don&rsquo;t even get me started. They&rsquo;re a one-way ticket to flaky tests and accidentally messing up your colleagues&rsquo; work. Nightmare fuel.</p>
<p>Coming from Go, I got spoiled by integration tests that gave me rock-solid confidence. I craved that same feeling in my TypeScript projects. In a <a href="https://nikolamilovic.com/posts/integration-testing-node-postgres-vitest-testcontainers/">previous post</a>, we explored using <code>testcontainers</code> to spin up pristine, isolated Postgres instances for our tests. It&rsquo;s a fantastic approach, giving you high fidelity by testing against the real deal (well, a Dockerized version). We even discussed ways to optimize the startup time.</p>
<p>But here&rsquo;s the thing: if you&rsquo;re like me and embrace a Test Driven Development (TDD) workflow, <em>every millisecond counts</em>.</p>
<p>(As always, the final project code can be found over at the <a href="https://github.com/Nikola-Milovic/blog-projects/tree/master/fun-and-sane-tdd-with-node-pglite">github repo</a>)</p>
<blockquote class="warning"><p>I am still evaluating and playing around with this approach, so please do only consider it as an educational post rather than a guide.</p>
</blockquote>
<h3 id="tdd-not-just-a-buzzword-its-about-flow">TDD: Not Just a Buzzword, It&rsquo;s About Flow</h3>
<p>What <em>is</em> TDD, really? You&rsquo;ll find plenty of formal definitions, often involving strict red-green-refactor cycles and meticulous unit testing. Kent Beck, a key figure in TDD, described it as a way to &ldquo;think through your design before you write your functional code.&rdquo;</p>
<p>Honestly, many resources make TDD sound overly rigid, almost tedious. That&rsquo;s fine if your project demands extreme meticulousness, but for many of us, especially working on CRUD heavy apps or exploring new features, a more pragmatic approach works wonders.</p>
<p>My take on TDD? It&rsquo;s about <strong>developing your code <em>alongside</em> your tests</strong>. Tests aren&rsquo;t just safety nets for the future; they&rsquo;re your <em>active guide</em> during development. They help you shape the functionality, catch edge cases early, and save you the endless cycle of starting your app, hitting it with <code>curl</code>, cleaning the database, and repeating. I tend to lean heavily on <em>integration tests</em> – testing slices of functionality, often involving the database, because if the data ends up correct in my source of truth (the DB), I can be fairly confident that it&rsquo;s working as intended.</p>
<p>This kind of workflow boosts my velocity and confidence immensely. But&hellip; it hinges on a <strong>fast feedback loop</strong>. Waiting even a few seconds for tests to spin up can break your concentration and kill momentum. That &ldquo;Time To Result&rdquo; (TTR, yeah, I made that up) needs to be <em>fast</em>.</p>
<p>Last post I showcased <code>testcontainers</code> approach, which is great and gets you close to a tight feedback loop (~2-4s initial setup, then subsequent test cases are much faster thanks to snapshots), that initial container spin-up time can still feel sluggish, especially if you want to run many test suites in parallel. What if we could make it <em>even faster</em>? (The answer is, <em>maybe</em>, this is more of an experiment rather than a recommendation on my part)</p>
<h3 id="enter-pglite-postgres-in-your-pocket-almost">Enter PGLite: Postgres in Your Pocket (Almost!)</h3>
<p>Recently, I stumbled upon something that felt like a game changer for this workflow: <a href="https://github.com/electric-sql/pglite"><strong>PGLite</strong></a>.</p>
<p>Imagine Postgres, but compiled to <a href="https://webassembly.org/">WASM</a> and packaged as a simple TypeScript library. That&rsquo;s PGLite. You can run a genuine Postgres engine right inside your Node.js (or Bun, or even browser!) process <em>without installing Postgres or any other dependencies</em>.</p>
<p>It&rsquo;s tiny (under 3MB gzipped!), incredibly fast to start, and supports many common Postgres extensions (like <code>pgvector</code>).</p>
<p>Getting started is ridiculously simple:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">PGlite</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@electric-sql/pglite&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Spin up an in-memory Postgres instance
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PGlite</span><span class="p">();</span> <span class="c1">// Or use a file path for persistence: new PGlite(&#39;data-directory&#39;)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// Run SQL queries!
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">&#34;select &#39;Hello from PGLite!&#39; as message;&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">rows</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// -&gt; [ { message: &#34;Hello from PGLite!&#34; } ]
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="c1">// Clean up
</span></span></span></code></pre></div><p>It can run entirely in memory (perfect for ephemeral test databases) or persist to the filesystem (if you plan to run parallel tests you will have to persist to disk, since there is an <a href="https://github.com/electric-sql/pglite/issues/324">issue</a> with running parallel in memory databases. Developed by the folks at <a href="https://electric-sql.com/">ElectricSQL</a>, it&rsquo;s designed for embedding Postgres, but its use cases are broader:</p>
<ul>
<li><strong>Unit and CI testing:</strong> This is our sweet spot! Lightning-fast setup and teardown. Create a unique, fresh Postgres instance for <em>every single test</em> if you want, in milliseconds.</li>
<li><strong>Local development:</strong> A lightweight alternative to running a full Postgres server.</li>
<li><strong>Remote/Web Containers:</strong> So small it&rsquo;s easily embeddable.</li>
<li><strong>Edge AI/RAG:</strong> Full <code>pgvector</code> support opens up interesting possibilities.</li>
</ul>
<h3 id="drizzle-the-orm-youll-actually-enjoy">Drizzle: The ORM You&rsquo;ll Actually Enjoy</h3>
<p>To make this setup even smoother, we&rsquo;ll pair PGLite with <a href="https://github.com/drizzle-team/drizzle-orm"><strong>Drizzle ORM</strong></a>. If you&rsquo;ve been burned by clunky, heavyweight ORMs before, give Drizzle a look.</p>
<p>Why Drizzle?</p>
<ul>
<li><strong>TypeScript Native:</strong> Feels right at home in a TS project. Great type safety.</li>
<li><strong>Lightweight:</strong> Tiny bundle size (~7.4kb minified+gzipped) and zero dependencies.</li>
<li><strong>Stellar DX:</strong> Seriously, it&rsquo;s a joy to use. As a big raw SQL fan, this has been the <em>only</em> ORM I&rsquo;ve actually enjoyed.</li>
<li><strong>Fast Migrations (with a twist):</strong> We&rsquo;ll use a neat trick to keep migrations snappy without manually generating files every time.</li>
</ul>
<h2 id="lets-get-practical-pglite--drizzle--vitest">Let&rsquo;s Get Practical: PGLite + Drizzle + Vitest</h2>
<p>Alright, theory&rsquo;s great, but let&rsquo;s see it in action. We&rsquo;ll modify the project from the <a href="https://github.com/Nikola-Milovic/blog-projects/tree/master/integration-testing-node-vitest-testcontainers">previous Testcontainers post</a> to use PGLite and Drizzle.</p>
<p><strong>1. Installation:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm install @electric-sql/pglite drizzle-orm
</span></span><span class="line"><span class="cl">npm install -D drizzle-kit vitest supertest @types/supertest
</span></span></code></pre></div><p><strong>2. Define Your Schema:</strong></p>
<p>Let&rsquo;s keep it simple with a Drizzle schema.</p>
<p><code>src/schema.ts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">pgTable</span><span class="p">,</span> <span class="nx">serial</span><span class="p">,</span> <span class="nx">varchar</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;drizzle-orm/pg-core&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">items</span> <span class="o">=</span> <span class="nx">pgTable</span><span class="p">(</span><span class="s2">&#34;items&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">id</span>: <span class="kt">serial</span><span class="p">(</span><span class="s2">&#34;id&#34;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl"> <span class="nx">name</span>: <span class="kt">varchar</span><span class="p">(</span><span class="s2">&#34;name&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">length</span>: <span class="kt">100</span> <span class="p">}).</span><span class="nx">notNull</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Add other tables here as needed
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">type</span> <span class="nx">Item</span> <span class="o">=</span> <span class="k">typeof</span> <span class="nx">items</span><span class="p">.</span><span class="nx">$inferSelect</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">type</span> <span class="nx">NewItem</span> <span class="o">=</span> <span class="k">typeof</span> <span class="nx">items</span><span class="p">.</span><span class="nx">$inferInsert</span><span class="p">;</span>
</span></span></code></pre></div><p><strong>3. The Magic Migration Helper:</strong></p>
<p>Normally, Drizzle uses migration files or <code>drizzle-kit push</code> to update your DB schema. But for rapid TDD, constantly generating migrations is a drag. Here&rsquo;s a helper function that uses <code>drizzle-kit</code>&rsquo;s <em>programmatic API</em> to figure out the SQL needed to create your schema from scratch. No migration files needed!</p>
<p><code>src/testing/db-helper.ts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="o">*</span> <span class="kr">as</span> <span class="nx">schema</span> <span class="kr">from</span> <span class="s2">&#34;../schema&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">drizzle</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;drizzle-orm/pglite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="kr">type</span> <span class="o">*</span> <span class="kr">as</span> <span class="nx">DrizzleKit</span> <span class="kr">from</span> <span class="s2">&#34;drizzle-kit/api&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="kr">type</span> <span class="p">{</span> <span class="nx">PGlite</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@electric-sql/pglite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Hack needed to dynamically import drizzle-kit&#39;s API
</span></span></span><span class="line"><span class="cl"><span class="c1">// See: https://github.com/drizzle-team/drizzle-orm/issues/2853
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">createRequire</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;node:module&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="kr">require</span> <span class="o">=</span> <span class="nx">createRequire</span><span class="p">(</span><span class="kr">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">generateDrizzleJson</span><span class="p">,</span> <span class="nx">generateMigration</span> <span class="p">}</span> <span class="o">=</span> <span class="kr">require</span><span class="p">(</span><span class="s2">&#34;drizzle-kit/api&#34;</span><span class="p">)</span> <span class="kr">as</span> <span class="k">typeof</span> <span class="nx">DrizzleKit</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * Generates SQL statements to create the schema defined in `src/schema.ts`
</span></span></span><span class="line"><span class="cl"><span class="cm"> * and applies them to the given PGLite client.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * This avoids needing migration files for tests, speeding up schema changes.
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span>
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">pushSchema</span><span class="p">(</span><span class="nx">client</span>: <span class="kt">PGlite</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">drizzle</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="p">{</span> <span class="nx">schema</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Generate an empty schema snapshot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// See: https://github.com/drizzle-team/drizzle-orm/issues/3913
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">const</span> <span class="nx">prevJson</span> <span class="o">=</span> <span class="nx">generateDrizzleJson</span><span class="p">({});</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Generate a snapshot based on your current schema definitions
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">const</span> <span class="nx">curJson</span> <span class="o">=</span> <span class="nx">generateDrizzleJson</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nx">schema</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nx">prevJson</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="c1">// Use the empty snapshot&#39;s ID
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kc">undefined</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;snake_case&#34;</span><span class="p">,</span> <span class="c1">// Or your preferred naming convention
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Compare the empty snapshot with the current schema to get all CREATE statements
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">const</span> <span class="nx">statements</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">generateMigration</span><span class="p">(</span><span class="nx">prevJson</span><span class="p">,</span> <span class="nx">curJson</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Apply the generated SQL statements
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Applying schema...&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">statement</span> <span class="k">of</span> <span class="nx">statements</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">execute</span><span class="p">(</span><span class="nx">statement</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Schema applied.&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><em>Benefits of this approach:</em></p>
<ul>
<li><strong>Rapid Schema Evolution:</strong> Change your Drizzle schema files, rerun tests, and the DB is updated instantly.</li>
<li><strong>No Migration File Clutter:</strong> Keeps your project cleaner.</li>
<li><strong>Performance:</strong> Applying one set of <code>CREATE</code> statements is faster than running potentially hundreds of historical migration files.</li>
<li><strong>Fantastic DX:</strong> Forget <code>drizzle-kit generate</code> or <code>push</code> during development, just code!</li>
</ul>
<p><strong>4. Update Your Tests:</strong></p>
<p>Now, let&rsquo;s update our Vitest setup (<code>src/server.test.ts</code>) to use PGLite. We&rsquo;ll use <code>beforeEach</code> and <code>afterEach</code> to ensure every test gets a fresh, migrated database instance.</p>
<p><code>src/server.test.ts</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">afterEach</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">beforeEach</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">describe</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">expect</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">vi</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;vitest&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">request</span> <span class="kr">from</span> <span class="s2">&#34;supertest&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">getDB</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./db.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="o">*</span> <span class="kr">as</span> <span class="nx">schema</span> <span class="kr">from</span> <span class="s2">&#34;./schema.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">pushSchema</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./testing/db-helper.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">PGlite</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@electric-sql/pglite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">drizzle</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;drizzle-orm/pglite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">eq</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;drizzle-orm&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">app</span> <span class="kr">from</span> <span class="s2">&#34;./server.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">PgDatabase</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;drizzle-orm/pg-core&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">TestAgent</span> <span class="kr">from</span> <span class="s2">&#34;supertest/lib/agent.js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Crucial: Mock the db module to intercept calls to getDB()
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">vi</span><span class="p">.</span><span class="nx">mock</span><span class="p">(</span><span class="s2">&#34;./db&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s2">&#34;Items API with PGLite&#34;</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">db</span>: <span class="kt">PgDatabase</span><span class="p">&lt;</span><span class="nt">any</span><span class="err">,</span> <span class="na">typeof</span> <span class="na">schema</span><span class="p">&gt;;</span> <span class="c1">// Use the correct type
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">let</span> <span class="nx">client</span>: <span class="kt">PGlite</span><span class="p">;</span> <span class="c1">// Hold the PGLite client instance
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">let</span> <span class="nx">agent</span>: <span class="kt">TestAgent</span><span class="p">;</span> <span class="c1">// Supertest agent
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Setup a fresh PGLite instance and apply schema before each test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PGlite</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">   <span class="c1">// You might want to specify a consistent temp directory:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="c1">// dataDir: `./.pglite-test-data/${dbName}` or `/tmp`
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">agent</span> <span class="o">=</span> <span class="nx">request</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span> <span class="c1">// Create supertest agent
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Apply the schema using our helper
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">await</span> <span class="nx">pushSchema</span><span class="p">(</span><span class="nx">client</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Create the Drizzle instance connected to PGLite
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">db</span> <span class="o">=</span> <span class="nx">drizzle</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="p">{</span> <span class="nx">schema</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Mock getDB() to return *this specific test&#39;s* Drizzle instance
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="c1">// Use `vi.mocked` for type safety
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">getDB</span><span class="p">).</span><span class="nx">mockReturnValue</span><span class="p">(</span><span class="nx">db</span> <span class="kr">as</span> <span class="kt">any</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Close the PGLite client and clean up mocks after each test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">afterEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="c1">// Release resources
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">vi</span><span class="p">.</span><span class="nx">clearAllMocks</span><span class="p">();</span> <span class="c1">// Reset mocks
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="c1">// Consider adding cleanup for the data directory if you specified one
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s2">&#34;POST /items should create an item&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">newItemName</span> <span class="o">=</span> <span class="s2">&#34;Test Item PGLite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">agent</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">send</span><span class="p">({</span> <span class="nx">name</span>: <span class="kt">newItemName</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">201</span><span class="p">);</span> <span class="c1">// Assert HTTP status code
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">newItemName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Verify directly in the DB for extra confidence
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">dbResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">select</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">schema</span><span class="p">.</span><span class="nx">items</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">eq</span><span class="p">(</span><span class="nx">schema</span><span class="p">.</span><span class="nx">items</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span> <span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">newItemName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s2">&#34;GET /items/:id should retrieve an existing item&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Arrange: Insert an item directly
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">insertResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">insert</span><span class="p">(</span><span class="nx">schema</span><span class="p">.</span><span class="nx">items</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">values</span><span class="p">({</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Get Me PGLite&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">returning</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">itemId</span> <span class="o">=</span> <span class="nx">insertResult</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Act: Request the item
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">agent</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="sb">`/items/</span><span class="si">${</span><span class="nx">itemId</span><span class="si">}</span><span class="sb">`</span><span class="p">).</span><span class="nx">expect</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Assert
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">itemId</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="s2">&#34;Get Me PGLite&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s2">&#34;GET /items/:id should return 404 for non-existent item&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">agent</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s2">&#34;/items/99999&#34;</span><span class="p">).</span><span class="nx">expect</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s2">&#34;POST /items should return 400 if name is missing&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">agent</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">).</span><span class="nx">send</span><span class="p">({}).</span><span class="nx">expect</span><span class="p">(</span><span class="mi">400</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p><strong>5. Run Your Tests:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm <span class="nb">test</span>
</span></span></code></pre></div><h3 id="the-need-for-speed-benchmarks">The Need for Speed: Benchmarks</h3>
<p>Okay, talk is cheap. Let&rsquo;s look at some rough numbers. These were run on my machine, so your mileage may wary, but they illustrate the difference.</p>
<p><strong>Testcontainers (with snapshotting after initial setup):</strong></p>
<pre tabindex="0"><code>&gt; vitest run
... (Container startup logs) ...
 ✓ src/server.test.ts (4 tests) 4141ms
   ✓ Items API &gt; POST /items should create an item  459ms
   ✓ Items API &gt; GET /items/:id should retrieve an existing item 159ms
   ✓ Items API &gt; GET /items/:id should return 404 for non-existent item 132ms
   ✓ Items API &gt; POST /items should return 400 if name is missing 154ms

 Test Files  1 passed (1)
      Tests  4 passed (4)
   Duration  4.45s (transform 32ms, setup 0ms, collect 159ms, tests 4.14s, environment 0ms, prepare 40ms)
</code></pre><p>Using <code>hyperfine</code> for 10 runs:</p>
<pre tabindex="0"><code>
Time (mean ± σ):      4.773 s ±  0.412 s    [User: 2.730 s, System: 0.991 s]
Range (min … max):    4.333 s …  5.772 s    10 runs
</code></pre><p><strong>PGLite (fresh instance per test, no optimizations yet):</strong></p>
<pre tabindex="0"><code>&gt; vitest run
 ✓ src/server.test.ts (4 tests) 2146ms
   ✓ Items API &gt; POST /items should create an item  576ms
   ✓ Items API &gt; GET /items/:id should retrieve an existing item  503ms
   ✓ Items API &gt; GET /items/:id should return 404 for non-existent item  516ms
   ✓ Items API &gt; POST /items should return 400 if name is missing  550ms

 Test Files  1 passed (1)
      Tests  4 passed (4)
   Duration  2.58s (transform 39ms, setup 0ms, collect 273ms, tests 2.15s, environment 0ms, prepare 42ms)
</code></pre><p>Using <code>hyperfine</code> for 10 runs:</p>
<pre tabindex="0"><code>Time (mean ± σ):      2.822 s ±  0.070 s    [User: 5.768 s, System: 1.766 s]
Range (min … max):    2.748 s …  2.946 s    10 runs
</code></pre><p><strong>The Verdict (Round 1):</strong> PGLite is already significantly faster overall (~2.8s vs ~4.8s) mainly because we skip the Docker container boot time entirely! Each test takes a bit longer individually because it&rsquo;s initializing PGLite <em>and</em> applying the schema every time. Can we do better?</p>
<h3 id="even-faster-enter-reusable-database">Even Faster? Enter reusable database</h3>
<p>Just like with Testcontainers, we can optimize further by setting up the database <em>once</em>, applying the schema, taking a &ldquo;snapshot,&rdquo; and then quickly restoring that clean state before each test. PGLite doesn&rsquo;t have a direct snapshot command, but we can achieve a similar effect with some filesystem manipulation.</p>
<blockquote class="warning"><p>I am still experimenting with this approach, it does net us some speed benefits but I cannot guarantee its complete correctness and safety, it&rsquo;s easy to setup so you can try it out but be wary. If you have any ideas how to improve this implementation please leave a comment or hit me up somewhere</p>
</blockquote>
<p><strong>1. Update the DB Helper:</strong></p>
<p>Add these functions to <code>src/testing/db-helper.ts</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="kr">type</span> <span class="p">{</span> <span class="nx">PGlite</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;@electric-sql/pglite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">snapshot</span><span class="p">(</span><span class="nx">client</span>: <span class="kt">PGlite</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">client</span><span class="p">.</span><span class="nx">dumpDataDir</span><span class="p">(</span><span class="s2">&#34;none&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">async</span> <span class="kd">function</span> <span class="nx">restoreSnapshot</span><span class="p">(</span><span class="nx">snapshot</span>: <span class="kt">File</span> <span class="o">|</span> <span class="nx">Blob</span><span class="p">)</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">PGlite</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">clone</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">File</span><span class="p">([</span><span class="nx">Buffer</span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="k">await</span> <span class="nx">snapshot</span><span class="p">.</span><span class="nx">arrayBuffer</span><span class="p">())],</span> <span class="s2">&#34;snapshot&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="kr">type</span><span class="o">:</span> <span class="nx">snapshot</span><span class="p">.</span><span class="kr">type</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">new</span> <span class="nx">PGlite</span><span class="p">({</span> <span class="nx">loadDataDir</span>: <span class="kt">clone</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>2. Modify the Test Setup:</strong></p>
<p>We&rsquo;ll now use <code>beforeAll</code> to set up <em>one</em> PGLite instance for the entire test file, apply the schema, and take the snapshot. <code>beforeEach</code> will simply restore the snapshot.</p>
<p><code>src/server.test.ts</code> (Changes highlighted)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ... other imports
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">beforeAll</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl"> <span class="nx">afterAll</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl"> <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;vitest&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="c1">// ... other imports
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">pushSchema</span><span class="p">,</span> <span class="nx">snapshot</span><span class="p">,</span> <span class="nx">restoreSnapshot</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./testing/db-helper&#34;</span><span class="p">;</span> 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">vi</span><span class="p">.</span><span class="nx">mock</span><span class="p">(</span><span class="s2">&#34;./db&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s2">&#34;Items API with PGLite&#34;</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">db</span>: <span class="kt">PgDatabase</span><span class="p">&lt;</span><span class="nt">any</span><span class="err">,</span> <span class="na">typeof</span> <span class="na">schema</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">client</span>: <span class="kt">PGlite</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">agent</span>: <span class="kt">TestAgent</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">snapshottedDB</span>: <span class="kt">File</span> <span class="o">|</span> <span class="nx">Blob</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">beforeAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PGlite</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">   <span class="c1">// You might want to specify a consistent temp directory:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="c1">// dataDir: `./.pglite-test-data/${dbName}` or `/tmp`
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Apply the schema once
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">await</span> <span class="nx">pushSchema</span><span class="p">(</span><span class="nx">client</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Take a snapshot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">snapshottedDB</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">snapshot</span><span class="p">(</span><span class="nx">client</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Setup a fresh PGLite instance and apply schema before each test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">agent</span> <span class="o">=</span> <span class="nx">request</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span> <span class="c1">// Create supertest agent
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="nx">client</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">restoreSnapshot</span><span class="p">(</span><span class="nx">snapshottedDB</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Create the Drizzle instance connected to PGLite
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">db</span> <span class="o">=</span> <span class="nx">drizzle</span><span class="p">(</span><span class="nx">client</span><span class="p">,</span> <span class="p">{</span> <span class="nx">schema</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Mock getDB() to return *this specific test&#39;s* Drizzle instance
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="c1">// Use `vi.mocked` for type safety
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">getDB</span><span class="p">).</span><span class="nx">mockReturnValue</span><span class="p">(</span><span class="nx">db</span> <span class="kr">as</span> <span class="kt">any</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Close the PGLite client and clean up mocks after each test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">afterEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span> <span class="c1">// Release resources
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">vi</span><span class="p">.</span><span class="nx">clearAllMocks</span><span class="p">();</span> <span class="c1">// Reset mocks
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Consider adding cleanup for the data directory if you specified one
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="c1">// afterAll
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s2">&#34;POST /items should create an item&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">newItemName</span> <span class="o">=</span> <span class="s2">&#34;Test Item PGLite&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">agent</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">send</span><span class="p">({</span> <span class="nx">name</span>: <span class="kt">newItemName</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">201</span><span class="p">);</span> <span class="c1">// Assert HTTP status code
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">newItemName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Verify directly in the DB for extra confidence
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="c1">// And to make sure the snapshot is working
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">dbResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">select</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">schema</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">newItemName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s2">&#34;GET /items/:id should retrieve an existing item&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Arrange: Insert an item directly
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">insertResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">insert</span><span class="p">(</span><span class="nx">schema</span><span class="p">.</span><span class="nx">items</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">values</span><span class="p">({</span> <span class="nx">name</span><span class="o">:</span> <span class="s2">&#34;Get Me PGLite&#34;</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">returning</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">itemId</span> <span class="o">=</span> <span class="nx">insertResult</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Act: Request the item
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">agent</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="sb">`/items/</span><span class="si">${</span><span class="nx">itemId</span><span class="si">}</span><span class="sb">`</span><span class="p">).</span><span class="nx">expect</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Verify directly in the DB for extra confidence
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="c1">// And to make sure the snapshot is working
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">dbResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">select</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">schema</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="s2">&#34;Get Me PGLite&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Assert
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">itemId</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="s2">&#34;Get Me PGLite&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p><strong>Let&rsquo;s run the benchmarks again!</strong></p>
<p><strong>PGLite (with reusable database):</strong></p>
<pre tabindex="0"><code>➜ npm run test

&gt; vitest run

 ✓ src/server.test.ts (4 tests) 1331ms
   ✓ Items API with PGLite &gt; POST /items should create an item 199ms
   ✓ Items API with PGLite &gt; GET /items/:id should retrieve an existing item 192ms
   ✓ Items API with PGLite &gt; GET /items/:id should return 404 for non-existent item 182ms
   ✓ Items API with PGLite &gt; POST /items should return 400 if name is missing 189ms
</code></pre><p>Using <code>hyperfine</code> for 10 runs:</p>
<pre tabindex="0"><code>Time (mean ± σ):      1.940 s ±  0.022 s    [User: 5.570 s, System: 1.289 s]
Range (min … max):    1.911 s …  1.987 s    10 runs
</code></pre><p><a href="https://www.youtube.com/watch?v=KEc8BQK-rbA"><strong>5 BIG BOOMS FOR THIS SPEED!</strong></a> Look at that difference! From ~4.8s with Testcontainers down to ~1.3s with PGLite snapshotting. This is the kind of speed that keeps you in the TDD flow state.</p>
<p>One big caveat is that these are still running sequentially, so with a lot of tests, it could add up and become even slower than the <code>testcontainers</code> version. I&rsquo;ll make a follow up post on how we could approach architecting our code to make sure it&rsquo;s testable and parallelizable. Since the current approach relies on mocking global accessors like <code>getDB</code> which make it impossible to parallelize.</p>
<h3 id="final-thoughts--caveats">Final Thoughts &amp; Caveats</h3>
<ol>
<li><strong>Testcontainers is Still Awesome:</strong> Let me be clear, <code>testcontainers</code> is a phenomenal tool. For testing against other services (Redis, Kafka, other databases) or when you need the <em>absolute highest fidelity</em> with a specific Postgres version/extension setup provided by Docker, it&rsquo;s still my go-to. This PGLite approach is specifically optimized for speeding up the Node.js &lt;-&gt; Postgres interaction during TDD experimentation sessions.</li>
<li><strong>PGLite Fidelity:</strong> While PGLite <em>is</em> running actual Postgres compiled to WASM, it&rsquo;s newer technology. Be mindful that there <em>could</em> be subtle differences or unsupported features compared to a native Postgres installation or a Docker image. I haven&rsquo;t hit any issues yet for typical web app CRUD operations, but always test thoroughly and be aware of the possibility. Don&rsquo;t let flaky tests give you false confidence.</li>
<li><strong>Potentionally slower</strong> for larger test suites. I am still not certain that this approach scales better than the <code>testcontainers</code> + snapshotting, since the bottleneck in that case is the startup time of the container, while subsequent tests run quite quickly. Not sure if the <code>fs</code> operations and the spinning up of <code>PGlite</code> instances will actually prove to be a bigger bottleneck than the initial startup time of <code>testcontainers</code>. But the big benefit of this approach is that it allows parallel execution of a test suite.</li>
</ol>
<p>This PGLite + Drizzle + Vitest combination, especially with the snapshotting(-ish) technique, has significantly improved my Node.js TDD experience. It brings back that snappy, responsive feeling I loved from Go (with <code>PGlite</code> it&rsquo;s even better than Go), making test-driven development truly enjoyable and productive.</p>
<hr>
<p><strong>Give these amazing projects a star!</strong> This workflow wouldn&rsquo;t be possible without them:</p>
<ul>
<li><a href="https://github.com/electric-sql/pglite">pglite</a> ⭐</li>
<li><a href="https://github.com/drizzle-team/drizzle-orm">drizzle-orm</a> ⭐</li>
<li><a href="https://github.com/vitest-dev/vitest">vitest</a> ⭐</li>
</ul>
<p><strong>Bonus Cool Project:</strong> <em>(Not sponsored, just want to make it a habit to share a cool project with every post)</em></p>
<ul>
<li><a href="https://github.com/tinacms/tinacms">TinaCMS</a>: An open-source, Git-backed headless CMS with visual editing. Really interesting approach if you need content management.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Integration Testing Node.js Postgres interaction Like You Mean It (with Vitest &amp; Testcontainers)</title>
      <link>https://nikolamilovic.com/posts/integration-testing-node-postgres-vitest-testcontainers/</link>
      <pubDate>Tue, 15 Apr 2025 17:43:38 +0200</pubDate>
      <guid>https://nikolamilovic.com/posts/integration-testing-node-postgres-vitest-testcontainers/</guid>
      <description>Integration Testing Node.js Postgres interaction Like You Mean It (with Vitest &amp;amp; Testcontainers) Look, unit tests are great for checking isolated logic. But eventually, you need to know if your code actually works with a real database, message queue, or whatever external service you depend on. Mocking everything is fragile and often hides bugs that only show up when things get real. Shared dev databases? A recipe for flaky tests and stepping on colleagues&amp;rsquo; toes.</description>
      <content:encoded><![CDATA[<h1 id="integration-testing-nodejs-postgres-interaction-like-you-mean-it-with-vitest--testcontainers">Integration Testing Node.js Postgres interaction Like You Mean It (with Vitest &amp; Testcontainers)</h1>
<p>Look, unit tests are great for checking isolated logic. But eventually, you need to know if your code <em>actually</em> works with a real database, message queue, or whatever external service you depend on. Mocking everything is fragile and often hides bugs that only show up when things get real. Shared dev databases? A recipe for flaky tests and stepping on colleagues&rsquo; toes.</p>
<p>Coming from Go, I got used to writing integration tests that gave me real confidence. I wanted that same feeling in my TypeScript projects. The answer? <strong>Testcontainers</strong>.</p>
<h2 id="the-core-idea-real-dependencies-isolated-tests">The Core Idea: Real Dependencies, Isolated Tests</h2>
<p>The plan is simple:</p>
<ol>
<li><strong>Spin up real dependencies</strong> (like Postgres, Redis, etc.) inside Docker containers <em>for your tests</em>.</li>
<li>Use a library like <a href="https://github.com/testcontainers/testcontainers-node">Testcontainers for Node.js</a> to manage these containers programmatically.</li>
<li>Run your migrations or setup scripts against the fresh container to establish a baseline state.</li>
<li><strong>Snapshot</strong> the clean, migrated database state (if your DB module supports it, like <code>@testcontainers/postgresql</code>). Alternatively, investigate the <code>docker commit</code> method if snapshotting isn&rsquo;t built-in.</li>
<li>For each test (or test suite), <strong>restore</strong> from that snapshot to get a perfectly clean slate, ensuring test isolation.</li>
<li>Run your test logic against the application, which talks to the containerized dependency.</li>
<li>Tear down the container(s) when done.</li>
</ol>
<p>This gives you the best of both worlds: the realism of testing against the actual database software you use in production, and the isolation needed for reliable, repeatable tests.</p>
<p><strong>Why not just use SQLite or an in-memory DB?</strong></p>
<p>Because it&rsquo;s <em>not the same</em>. You might use database-specific features, syntax quirks, or transaction behaviors that SQLite doesn&rsquo;t replicate. Testing against a different database than production is asking for trouble.</p>
<blockquote class="warning"><p>Never use a different DB for development/testing than what you use in production. If you&rsquo;re using Postgres in prod, use Postgres containers for testing. Don&rsquo;t cheat with SQLite just because it seems easier. Your ORM won&rsquo;t save you from every difference.</p>
</blockquote>
<h2 id="lets-build-it-a-simple-crud-example">Let&rsquo;s Build It: A Simple CRUD Example</h2>
<p>We&rsquo;ll set up a dead simple Express app with basic CRUD operations for &ldquo;items&rdquo; stored in a Postgres database. Then, we&rsquo;ll write integration tests for it using Vitest and Testcontainers.</p>
<p><strong>(You can find the <a href="https://github.com/Nikola-Milovic/blog-projects/tree/master/integration-testing-node-vitest-testcontainers">complete project code over at github</a>)</strong></p>
<p><strong>1. Project Setup</strong></p>
<p>Make sure you have Node.js and Docker installed.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir node-testcontainers-example
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> node-testcontainers-example
</span></span><span class="line"><span class="cl">npm init -y
</span></span><span class="line"><span class="cl">npm install express pg <span class="c1"># Or your preferred framework/DB driver</span>
</span></span><span class="line"><span class="cl">npm install -D typescript @types/node @types/express @types/pg ts-node nodemon vitest testcontainers @testcontainers/postgresql <span class="c1"># Dev dependencies</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Initialize tsconfig.json</span>
</span></span><span class="line"><span class="cl">npx tsc --init --rootDir src --outDir dist --esModuleInterop --resolveJsonModule --lib esnext --module nodenext --allowJs <span class="nb">true</span> --noImplicitAny <span class="nb">true</span>
</span></span></code></pre></div><p>Create a simple <code>src/server.ts</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">express</span><span class="p">,</span> <span class="p">{</span> <span class="kr">type</span> <span class="nx">Response</span><span class="p">,</span> <span class="kr">type</span> <span class="nx">Request</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;express&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">getDB</span> <span class="p">}</span> <span class="kr">from</span> <span class="s2">&#34;./db&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nx">express</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nx">json</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">req</span>: <span class="kt">Request</span><span class="p">,</span> <span class="nx">res</span>: <span class="kt">Response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="p">{</span> <span class="nx">name</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">400</span><span class="p">).</span><span class="nx">send</span><span class="p">({</span> <span class="nx">error</span><span class="o">:</span> <span class="s2">&#34;Name is required&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">getDB</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">   <span class="s2">&#34;INSERT INTO items(name) VALUES($1) RETURNING *&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="p">[</span><span class="nx">name</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">201</span><span class="p">).</span><span class="nx">send</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">rows</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span> 
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">500</span><span class="p">).</span><span class="nx">send</span><span class="p">({</span> <span class="nx">error</span><span class="o">:</span> <span class="s2">&#34;Failed to create item&#34;</span> <span class="p">});</span> 
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">app</span><span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s2">&#34;/items/:id&#34;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">(</span><span class="nx">req</span>: <span class="kt">Request</span><span class="p">,</span> <span class="nx">res</span>: <span class="kt">Response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">db</span> <span class="o">=</span> <span class="nx">getDB</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">&#34;SELECT * FROM items WHERE id = $1&#34;</span><span class="p">,</span> <span class="p">[</span><span class="nx">id</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">404</span><span class="p">).</span><span class="nx">send</span><span class="p">({</span> <span class="nx">error</span><span class="o">:</span> <span class="s2">&#34;Item not found&#34;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">   <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">rows</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span> 
</span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">500</span><span class="p">).</span><span class="nx">send</span><span class="p">({</span> <span class="nx">error</span><span class="o">:</span> <span class="s2">&#34;Failed to retrieve item&#34;</span> <span class="p">});</span> 
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Only start listening if the file is run directly
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="kr">require</span><span class="p">.</span><span class="nx">main</span> <span class="o">===</span> <span class="nx">module</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">port</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">3000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="nx">app</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="nx">port</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Server listening on port </span><span class="si">${</span><span class="nx">port</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// You&#39;d typically run migrations here on real app startup
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="c1">// For this example, we assume the table exists or is created by tests/manually
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">app</span><span class="p">;</span>
</span></span></code></pre></div><p>And <code>src/db.ts</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Pool</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;pg&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Default connection for manual running or real deployment
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">const</span> <span class="nx">pool</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pool</span><span class="p">({</span>
</span></span><span class="line"><span class="cl"> <span class="nx">connectionString</span>: <span class="kt">process.env.DATABASE_URL</span> <span class="o">||</span> <span class="s1">&#39;postgresql://postgres:postgres@localhost:5432/postgres&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// We export a function to get the pool. This allows us to easily mock
</span></span></span><span class="line"><span class="cl"><span class="c1">// it in tests to point to our Testcontainers-managed database instead.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">const</span> <span class="nx">getDB</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">pool</span><span class="p">;</span>
</span></span></code></pre></div><p><strong>Manual Verification (Optional)</strong></p>
<p>To verify our server manually, we&rsquo;d first need a running Postgres instance and the <code>items</code> table created. You could achieve this using Docker:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Start a Postgres container</span>
</span></span><span class="line"><span class="cl">docker run -d <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name my-pg-container <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -e <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>postgres <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -p 5432:5432 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  postgres:17
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Execute the CREATE TABLE command inside the container</span>
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> -it my-pg-container psql -U postgres -d postgres -c <span class="s2">&#34;CREATE TABLE IF NOT EXISTS items (id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL);&#34;</span>
</span></span></code></pre></div><p>Then, add a start script to <code>package.json</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl">  <span class="s2">&#34;scripts&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;start&#34;</span><span class="p">:</span> <span class="s2">&#34;tsc &amp;&amp; node ./dist/server.js&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span></code></pre></div><p>Start the server (<code>npm start</code>) and use <code>curl</code> to interact with it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">➜ npm start
</span></span><span class="line"><span class="cl">&gt; tsc <span class="o">&amp;&amp;</span> node ./dist/server.js
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Server listening on port <span class="m">3000</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">➜ curl -X POST http://localhost:3000/items -H <span class="s2">&#34;Content-Type: application/json&#34;</span> -d <span class="s1">&#39;{&#34;name&#34;: &#34;do the dishes&#34;}&#39;</span>
</span></span><span class="line"><span class="cl">&gt; <span class="o">{</span><span class="s2">&#34;id&#34;</span>:1,<span class="s2">&#34;name&#34;</span>:<span class="s2">&#34;do the dishes&#34;</span><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">➜ curl http://localhost:3000/items/1
</span></span><span class="line"><span class="cl">&gt; <span class="o">{</span><span class="s2">&#34;id&#34;</span>:1,<span class="s2">&#34;name&#34;</span>:<span class="s2">&#34;do the dishes&#34;</span><span class="o">}</span>
</span></span></code></pre></div><p>This works, but manually managing the database for testing is cumbersome and error-prone. Let&rsquo;s automate it. (Remember to stop and remove the manual container: <code>docker stop my-pg-container &amp;&amp; docker rm my-pg-container</code>)</p>
<h2 id="automated-testing-with-testcontainers">Automated Testing with Testcontainers</h2>
<p>Add a <code>vitest.config.ts</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// vitest.config.ts
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">defineConfig</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;vitest/config&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">defineConfig</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="nx">test</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">globals</span>: <span class="kt">true</span><span class="p">,</span> <span class="c1">// Use Vitest globals (describe, it, etc.)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">setupFiles</span><span class="o">:</span> <span class="p">[],</span> <span class="c1">// Optional: setup files for tests
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">environment</span><span class="o">:</span> <span class="s1">&#39;node&#39;</span><span class="p">,</span> <span class="c1">// Specify Node environment
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// Increase timeout for container startup, snapshotting etc.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">testTimeout</span>: <span class="kt">60000</span><span class="p">,</span> <span class="c1">// 60 seconds
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">hookTimeout</span>: <span class="kt">60000</span><span class="p">,</span> <span class="c1">// 60 seconds for hooks too
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p><strong>2. The Test Helper (<code>PostgresContainerManager</code>)</strong></p>
<p>Managing the container lifecycle (start, migrate, snapshot, restore, stop) and ensuring a clean database state for each test can be repetitive. Let&rsquo;s encapsulate this logic in a <code>PostgresContainerManager</code> helper class. This class will handle starting a Postgres container, running initial migrations within it, creating a baseline snapshot of that state, and providing functions to get a fresh database connection reset to that baseline for each test.</p>
<p><em>Heads up:</em> The snapshot feature requires <code>testcontainers</code> version <code>10.23.0</code> or later for the Postgres module (check your <code>package.json</code>!).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// src/testing/db-helper.ts
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">PostgreSqlContainer</span><span class="p">,</span> <span class="kr">type</span> <span class="nx">StartedPostgreSqlContainer</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;@testcontainers/postgresql&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Client</span><span class="p">,</span> <span class="nx">Pool</span><span class="p">,</span> <span class="kr">type</span> <span class="nx">PoolClient</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;pg&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Interface for what setupTestDatabase will return
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">interface</span> <span class="nx">TestDatabase</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">db</span>: <span class="kt">PoolClient</span><span class="p">;</span> <span class="c1">// Test should use this client
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">container</span>: <span class="kt">StartedPostgreSqlContainer</span><span class="p">;</span> <span class="c1">// Reference to the container
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">cleanup</span><span class="o">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">void</span><span class="p">&gt;;</span> <span class="c1">// Function to release the client/pool
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Manages the PostgreSQL container lifecycle for tests
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">export</span> <span class="kr">class</span> <span class="nx">PostgresContainerManager</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">private</span> <span class="kr">static</span> <span class="nx">instance</span>: <span class="kt">PostgresContainerManager</span> <span class="o">|</span> <span class="kc">null</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kr">private</span> <span class="nx">container</span>: <span class="kt">StartedPostgreSqlContainer</span> <span class="o">|</span> <span class="kc">null</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="kr">private</span> <span class="nx">snapshotName</span> <span class="o">=</span> <span class="s1">&#39;clean-db-snapshot&#39;</span><span class="p">;</span> <span class="c1">// Name for our baseline snapshot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Singleton pattern to potentially share container across test suites (though we scope it per-file here)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">private</span> <span class="kr">constructor</span><span class="p">()</span> <span class="p">{</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="kr">public</span> <span class="kr">static</span> <span class="nx">getInstance</span><span class="p">()</span><span class="o">:</span> <span class="nx">PostgresContainerManager</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">PostgresContainerManager</span><span class="p">.</span><span class="nx">instance</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">PostgresContainerManager</span><span class="p">.</span><span class="nx">instance</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PostgresContainerManager</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">PostgresContainerManager</span><span class="p">.</span><span class="nx">instance</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Starts container, runs migrations *in* the container DB, takes snapshot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">async</span> <span class="nx">initialize</span><span class="p">()</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">void</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Container already initialized.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Starting PostgreSQL container...&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">this</span><span class="p">.</span><span class="nx">container</span> <span class="o">=</span> <span class="k">await</span> <span class="k">new</span> <span class="nx">PostgreSqlContainer</span><span class="p">(</span><span class="s1">&#39;postgres:17&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">withDatabase</span><span class="p">(</span><span class="s1">&#39;test&#39;</span><span class="p">)</span> <span class="c1">// Use a specific DB name for testing
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="p">.</span><span class="nx">withUsername</span><span class="p">(</span><span class="s1">&#39;test-user&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">withPassword</span><span class="p">(</span><span class="s1">&#39;test-password&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">withExposedPorts</span><span class="p">(</span><span class="mi">5432</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Container started on port </span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">getMappedPort</span><span class="p">(</span><span class="mi">5432</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Connect directly to the containerized database to run migrations
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">migrationClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Client</span><span class="p">({</span> <span class="nx">connectionString</span>: <span class="kt">this.container.getConnectionUri</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">migrationClient</span><span class="p">.</span><span class="nx">connect</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">&#34;Running migrations...&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="c1">// In a real app, use your migration tool (node-pg-migrate, TypeORM migrations, etc.)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="c1">// For this example, we create the table directly:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="k">await</span> <span class="nx">migrationClient</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">                CREATE TABLE items (
</span></span></span><span class="line"><span class="cl"><span class="sb">                    id SERIAL PRIMARY KEY,
</span></span></span><span class="line"><span class="cl"><span class="sb">                    name VARCHAR(100) NOT NULL
</span></span></span><span class="line"><span class="cl"><span class="sb">                );
</span></span></span><span class="line"><span class="cl"><span class="sb">            `</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Migrations complete.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Migration failed:&#39;</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span> <span class="c1">// Fail fast if migrations don&#39;t work
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="k">await</span> <span class="nx">migrationClient</span><span class="p">.</span><span class="nx">end</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Take a snapshot of the database state *after* migrations
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Taking snapshot &#39;</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">snapshotName</span><span class="si">}</span><span class="sb">&#39;...`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">snapshot</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">snapshotName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Snapshot taken.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Restores the &#39;clean&#39; snapshot and provides a connection pool/client
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">async</span> <span class="nx">setupTestDatabase</span><span class="p">()</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">TestDatabase</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s1">&#39;Container not initialized. Call initialize() first.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="c1">// Restore the database to the state captured in the snapshot
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Restoring snapshot &#39;</span><span class="si">${</span><span class="k">this</span><span class="p">.</span><span class="nx">snapshotName</span><span class="si">}</span><span class="sb">&#39;...`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">restoreSnapshot</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">snapshotName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Snapshot restored.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   <span class="c1">// Create a *new pool* connecting to the restored database for this test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="c1">// This ensures connection isolation if tests run concurrently within the same file (though less common)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="kr">const</span> <span class="nx">testPool</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Pool</span><span class="p">({</span> <span class="nx">connectionString</span>: <span class="kt">this.container.getConnectionUri</span><span class="p">()</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">   <span class="kr">const</span> <span class="nx">testClient</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">testPool</span><span class="p">.</span><span class="nx">connect</span><span class="p">();</span> <span class="c1">// Get a client for the test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">   <span class="c1">// Cleanup function specific to this test&#39;s pool/client
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="kr">const</span> <span class="nx">cleanup</span> <span class="o">=</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="k">await</span> <span class="nx">testClient</span><span class="p">.</span><span class="nx">release</span><span class="p">();</span> <span class="c1">// Release client back to pool
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>     <span class="k">await</span> <span class="nx">testPool</span><span class="p">.</span><span class="nx">end</span><span class="p">();</span>      <span class="c1">// Close the pool
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">     <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Error during test DB cleanup:&#39;</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">   <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">   <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">db</span>: <span class="kt">testClient</span><span class="p">,</span> <span class="c1">// Provide the client to the test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">container</span>: <span class="kt">this.container</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">cleanup</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s1">&#39;Error setting up test database:&#39;</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="k">throw</span> <span class="nx">error</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Stops and removes the container
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kr">async</span> <span class="nx">teardown</span><span class="p">()</span><span class="o">:</span> <span class="nx">Promise</span><span class="p">&lt;</span><span class="nt">void</span><span class="p">&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Stopping PostgreSQL container...&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">   <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">stop</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">   <span class="k">this</span><span class="p">.</span><span class="nx">container</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">   <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Container stopped.&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="nx">PostgresContainerManager</span><span class="p">.</span><span class="nx">instance</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span> <span class="c1">// Reset singleton state
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><strong>3. Writing the Integration Test</strong></p>
<p>Now, let&rsquo;s write the integration test using Vitest and our helper. We&rsquo;ll use <code>supertest</code> to make HTTP requests to our Express app. The key parts are:</p>
<ul>
<li>Mocking our <code>db.ts</code> module so the application uses the test database connection provided by the helper (<code>testDb.db</code>).</li>
<li>Using Vitest hooks (<code>beforeAll</code>, <code>afterAll</code>, <code>beforeEach</code>, <code>afterEach</code>) to manage the container lifecycle and ensure test isolation via snapshot restoration.</li>
<li>Note: Using <code>beforeAll</code>/<code>afterAll</code> within the test file means Vitest&rsquo;s default parallel execution (by file) will likely spin up one independent container <em>per test file</em>, providing strong isolation but potentially using more resources.</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// src/server.test.ts
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">afterAll</span><span class="p">,</span> <span class="nx">afterEach</span><span class="p">,</span> <span class="nx">beforeAll</span><span class="p">,</span> <span class="nx">beforeEach</span><span class="p">,</span> <span class="nx">describe</span><span class="p">,</span> <span class="nx">expect</span><span class="p">,</span> <span class="nx">it</span><span class="p">,</span> <span class="nx">vi</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;vitest&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">request</span> <span class="kr">from</span> <span class="s1">&#39;supertest&#39;</span><span class="p">;</span> <span class="c1">// For making HTTP requests
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="kr">type</span> <span class="nx">TestAgent</span> <span class="kr">from</span> <span class="s1">&#39;supertest/lib/agent&#39;</span><span class="p">;</span> <span class="c1">// Type for supertest agent
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="nx">app</span> <span class="kr">from</span> <span class="s1">&#39;./server&#39;</span><span class="p">;</span> <span class="c1">// Our Express app
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">getDB</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./db&#39;</span><span class="p">;</span> <span class="c1">// The function we need to mock
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">import</span> <span class="p">{</span> <span class="nx">PostgresContainerManager</span><span class="p">,</span> <span class="kr">type</span> <span class="nx">TestDatabase</span> <span class="p">}</span> <span class="kr">from</span> <span class="s1">&#39;./testing/db-helper&#39;</span><span class="p">;</span> <span class="c1">// Our helper
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"><span class="c1">// This is crucial: tell Vitest to replace the real &#39;./db&#39; module
</span></span></span><span class="line"><span class="cl"><span class="c1">// with our mock, so we can control what getDB() returns in tests.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">vi</span><span class="p">.</span><span class="nx">mock</span><span class="p">(</span><span class="s1">&#39;./db&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;Items API&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="kr">const</span> <span class="nx">containerManager</span> <span class="o">=</span> <span class="nx">PostgresContainerManager</span><span class="p">.</span><span class="nx">getInstance</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nx">testDb</span>: <span class="kt">TestDatabase</span><span class="p">;</span> <span class="c1">// Will hold the connection/cleanup func for each test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="kd">let</span> <span class="nx">agent</span>: <span class="kt">TestAgent</span><span class="p">;</span>     <span class="c1">// Supertest agent for making requests
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Start the single container ONCE before all tests in this file
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">beforeAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">containerManager</span><span class="p">.</span><span class="nx">initialize</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="nx">agent</span> <span class="o">=</span> <span class="nx">request</span><span class="p">(</span><span class="nx">app</span><span class="p">);</span> <span class="c1">// Create supertest agent targeting our app
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">},</span> <span class="mi">60000</span><span class="p">);</span> <span class="c1">// Increase timeout for container init
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="c1">// Restore snapshot and get a fresh DB connection BEFORE EACH test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">beforeEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">testDb</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">containerManager</span><span class="p">.</span><span class="nx">setupTestDatabase</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Point the mocked getDB function to return our test database client
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">vi</span><span class="p">.</span><span class="nx">mocked</span><span class="p">(</span><span class="nx">getDB</span><span class="p">).</span><span class="nx">mockReturnValue</span><span class="p">(</span><span class="nx">testDb</span><span class="p">.</span><span class="nx">db</span><span class="p">);</span> 
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Clean up the test database connection AFTER EACH test
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">afterEach</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">testDb</span><span class="p">.</span><span class="nx">cleanup</span><span class="p">();</span> <span class="c1">// Release pool client and end pool
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">vi</span><span class="p">.</span><span class="nx">clearAllMocks</span><span class="p">();</span>     <span class="c1">// Reset mocks between tests
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="c1">// Stop the single container ONCE after all tests in this file are done
</span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">afterAll</span><span class="p">(</span><span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">containerManager</span><span class="p">.</span><span class="nx">teardown</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span> <span class="mi">60000</span><span class="p">);</span> <span class="c1">// Increase timeout for container teardown
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s1">&#39;POST /items should create an item&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">newItemName</span> <span class="o">=</span> <span class="s1">&#39;Test Item 1&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">agent</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;/items&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">send</span><span class="p">({</span> <span class="nx">name</span>: <span class="kt">newItemName</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">201</span><span class="p">);</span> <span class="c1">// Assert HTTP status code
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Assert response body
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBeDefined</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">newItemName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Optional but recommended: Verify directly in the DB
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">dbResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">testDb</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s1">&#39;SELECT * FROM items WHERE id = $1&#39;</span><span class="p">,</span> <span class="p">[</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">.</span><span class="nx">rows</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">dbResult</span><span class="p">.</span><span class="nx">rows</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">newItemName</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s1">&#39;GET /items/:id should retrieve an existing item&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1">// Arrange: Insert an item directly using the test DB client
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">insertResult</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">testDb</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="s2">&#34;INSERT INTO items(name) VALUES(&#39;Get Me&#39;) RETURNING id&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">itemId</span> <span class="o">=</span> <span class="nx">insertResult</span><span class="p">.</span><span class="nx">rows</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Act: Make request to the API endpoint
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">agent</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="sb">`/items/</span><span class="si">${</span><span class="nx">itemId</span><span class="si">}</span><span class="sb">`</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// Assert
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">id</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">itemId</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">expect</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="s1">&#39;Get Me&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s1">&#39;GET /items/:id should return 404 for non-existent item&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">agent</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="kr">get</span><span class="p">(</span><span class="s1">&#39;/items/99999&#39;</span><span class="p">)</span> <span class="c1">// Use an ID that almost certainly won&#39;t exist
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">404</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="nx">it</span><span class="p">(</span><span class="s1">&#39;POST /items should return 400 if name is missing&#39;</span><span class="p">,</span> <span class="kr">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">await</span> <span class="nx">agent</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;/items&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">   <span class="p">.</span><span class="nx">send</span><span class="p">({})</span> <span class="c1">// Send empty body
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>   <span class="p">.</span><span class="nx">expect</span><span class="p">(</span><span class="mi">400</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span></code></pre></div><p><strong>4. Running the Tests</strong></p>
<p>Add test scripts to your <code>package.json</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;scripts&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;start&#34;</span><span class="p">:</span> <span class="s2">&#34;tsc &amp;&amp; node ./dist/server.js&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;test&#34;</span><span class="p">:</span> <span class="s2">&#34;vitest run&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;test:watch&#34;</span><span class="p">:</span> <span class="s2">&#34;vitest&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ... other scripts
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Now run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">npm <span class="nb">test</span>
</span></span></code></pre></div><p>You should see Vitest start, the helper log messages about starting the container, running migrations, taking/restoring snapshots, and finally the test results passing.</p>
<h2 id="leveling-up">Leveling Up</h2>
<p><strong>1. Speeding Up Slow Migrations</strong></p>
<p>If your migrations take a <em>long</em> time to run, the initial <code>initialize</code> step can become a bottleneck. The snapshot helps for subsequent test <em>runs</em>, but the first one is still slow.</p>
<p><strong>Solution:</strong> Pre-build a Docker image with migrations already applied.</p>
<ol>
<li>
<p>Start a standard Postgres container manually: <code>docker run -d --name postgres-migrated -e POSTGRES_PASSWORD=password postgres:17</code></p>
</li>
<li>
<p>Connect to it and run your migrations.</p>
</li>
<li>
<p>Commit the container state to a new image: <code>docker commit postgres-migrated my-app/postgres-migrated:latest</code></p>
</li>
<li>
<p>Stop and remove the temporary container: <code>docker stop postgres-migrated &amp;&amp; docker rm postgres-migrated</code></p>
</li>
<li>
<p>In your <code>PostgresContainerManager</code>, change the image name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="c1">// In initialize()
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">this</span><span class="p">.</span><span class="nx">container</span> <span class="o">=</span> <span class="k">await</span> <span class="k">new</span> <span class="nx">PostgreSqlContainer</span><span class="p">(</span><span class="s1">&#39;my-app/postgres-migrated:latest&#39;</span><span class="p">)</span> <span class="c1">// Use your committed image
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// .withDatabase(...) // Ensure user/pass match how you set it up
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// ... rest of setup ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">.</span><span class="nx">start</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// You can likely SKIP the migration running step and snapshotting
</span></span></span><span class="line"><span class="cl"><span class="c1">// if the image already represents the clean, migrated state.
</span></span></span><span class="line"><span class="cl"><span class="c1">// Just start the container and proceed to setupTestDatabase.
</span></span></span><span class="line"><span class="cl"><span class="c1">// You might need a different helper logic for pre-built images.
</span></span></span><span class="line"><span class="cl"><span class="c1">// Remember to adjust initialize() to skip migrations/snapshotting if using this.
</span></span></span></code></pre></div></li>
</ol>
<blockquote class="note"><p>docker commit creates an image from a container&rsquo;s filesystem state. It&rsquo;s generally better for capturing installed software or data setup (like migrations) than for runtime state. Check the Docker commit docs for details.</p>
</blockquote>
<p><strong>Trade-offs:</strong> Faster test startup vs. managing another custom Docker image in your build process.</p>
<p><strong>2. Running Tests in Parallel</strong></p>
<p>Vitest runs test <em>files</em> in parallel by default. Because our <code>PostgresContainerManager</code> setup (<code>beforeAll</code>/<code>afterAll</code>) is scoped <em>within</em> a test suite, each test suite that follows this pattern will get its <em>own</em> independent Docker container.</p>
<ul>
<li><strong>Pro:</strong> True parallelism, maximum isolation between test suites.</li>
<li><strong>Con:</strong> Resource intensive. If you have many test files, you&rsquo;ll be running many database containers simultaneously. Make sure your machine (and CI environment) can handle it.</li>
</ul>
<p>If resource usage is a major concern, you <em>could</em> explore:</p>
<ul>
<li><strong>Shared Container, Schemas/DBs per Test File:</strong> Modify the <code>PostgresContainerManager</code> to be a true singleton across the entire test run (e.g., using Vitest&rsquo;s global setup). In <code>setupTestDatabase</code>, instead of restoring a snapshot (which resets the whole DB), you might create a unique <em>schema</em> or entirely new database <em>within the single container</em> for each test file (or even each <code>describe</code> block).</li>
<li><strong>Vitest <code>--no-parallel</code> or <code>--max-workers</code>:</strong> Force sequential execution or limit concurrency if parallelism causes issues or consumes too many resources.</li>
<li>Using technologies like <a href="https://pglite.dev/">PGLite</a>, (stay tuned for the next post ;) )</li>
</ul>
<h2 id="pros-and-cons-recap">Pros and Cons Recap</h2>
<p><strong>Pros:</strong></p>
<ul>
<li><strong>High Confidence:</strong> You&rsquo;re testing against the real database system.</li>
<li><strong>Catches Integration Bugs:</strong> Finds issues related to database interactions, transactions, constraints, etc., that mocks miss.</li>
<li><strong>Realistic Environment:</strong> Mimics production dependencies more closely.</li>
<li><strong>Isolated &amp; Repeatable:</strong> Each test gets a clean slate via snapshot restore, avoiding flakiness.</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><strong>Slower than Unit Tests:</strong> Starting containers and interacting with a real DB takes more time.</li>
<li><strong>Resource Usage:</strong> Docker containers consume CPU, RAM, and disk space, especially when ran in parallel.</li>
<li><strong>Dependency on Docker:</strong> Requires Docker to be installed and running where tests execute (local dev, CI).</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Setting up integration tests with Testcontainers is something I&rsquo;ve been incorporating into every project I am starting nowadays with Node.js, it gives me great velocity while developing and ease of mind when refactoring. The payoff in confidence and catching real-world bugs is huge. By managing container lifecycles programmatically and ensuring clean state via snapshots or unique databases per test, you can build a robust, reliable integration test suite for your Node.js applications. I&rsquo;d even argue that this is actually simpler than mocking since there isn&rsquo;t a good way (that I am aware of) to properly mock these database interactions without using an ORM that supports it or mocking the data layer representation you&rsquo;re using (<code>store</code>/<code>repository</code>/<code>dao</code>&rsquo;s)</p>
<p>Stop guessing if your database interaction code works – test it properly.</p>
<hr>
<p><strong>Give these amazing projects a star!</strong> This workflow wouldn&rsquo;t be possible without them:</p>
<ul>
<li><a href="https://github.com/vitest-dev/vitest">vitest</a> ⭐</li>
<li><a href="https://github.com/testcontainers/testcontainers-node">testcontainers-node</a> ⭐</li>
</ul>
<p><strong>Bonus Cool Project:</strong> <em>(Not sponsored, just want to make it a habit to share a cool project with every post)</em></p>
<ul>
<li><a href="https://github.com/webtui/webtui">WebTUI</a>: An open-source, modular CSS Library that brings the beauty of Terminal UIs to the browser</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
