Spawn | Bloghttps://docs.spawn.dev/enTest your PostgreSQL database like a sorcererhttps://docs.spawn.dev/blog/regression-tests/https://docs.spawn.dev/blog/regression-tests/Mon, 16 Feb 2026 00:00:00 GMT<aside aria-label="Human written"> <p aria-hidden="true"> Human written </p> <div> <p>Lovingly hand-crafted blog post, with occasional use of LLM’s to speed up creation of SQL examples.</p> </div> </aside> <p>Mark (aka Winsaucerer) here to show you how you can test your PostgreSQL database like a sorcerer. We are going to be using Spawn, a SQL build system supporting migrations and testing. You do <em>not</em> need to be using Spawn for migrations in order to use it for testing. Spawn does not require any extension installed. All you need is the <code dir="auto">spawn</code> CLI and a psql connection to the database for Spawn to connect through.</p> <p>Spawn was built to solve some migration pains I’ve experienced, but I happily discovered that when used for testing, it is <em>very</em> powerful. To show you some of that power, we’re going to use a contrived database example. It uses golden file testing to determine success. When the test runs, we capture the stdout and stderr output from psql, and compare that to expected output.</p> <p>Testing with Spawn involves these steps:</p> <ol> <li>Create a new test with <code dir="auto">spawn test new &#x3C;name></code> and fill out the test steps</li> <li>Check test outputs with <code dir="auto">spawn test run &#x3C;name></code> (or view the SQL that will be sent to psql via <code dir="auto">spawn test build &#x3C;name></code>)</li> <li>When outputs are as expected, create the golden file with <code dir="auto">spawn test expect &#x3C;name></code></li> <li>Run the test and compare to expected output with <code dir="auto">spawn test compare &#x3C;name></code></li> </ol> <p>For now, Spawn only supports connecting via psql, which means that you have access to all the features that psql provides.</p> <p>To get started, follow the Spawn install instructions:</p> <a href="https://docs.spawn.dev/getting-started/install/"> Install Spawn </a> <p>And then create a new folder on your system, and initialise a new project with a docker compose config ready for us to play with:</p> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span># inside your new folder:</span></div></div><div><div><span>spawn</span><span> </span><span>init</span><span> </span><span>--docker</span></div></div><div><div><span>docker</span><span> </span><span>compose</span><span> </span><span>up</span><span> </span><span>-d</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>You now have a running docker based PostgreSQL database and a <code dir="auto">spawn.toml</code> file configured to connect to it. We are not assuming that you are using Spawn or any other tool for migrations, so you can manually create and update the database by connecting directly using psql:</p> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>docker</span><span> </span><span>exec</span><span> </span><span>-ti</span><span> </span><span>postgres-db</span><span> </span><span>psql</span><span> </span><span>-U</span><span> </span><span>postgres</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <div><h2 id="create-the-database">Create the database</h2></div> <aside aria-label="Caution"> <p aria-hidden="true"> Caution </p> <div> <p>This post is not intended as an example of how to build an orders database. The design of this database is aimed at demonstrating how to test features. There are some design choices made here that I would <em>not</em> use for a serious orders database design.</p> </div> </aside> <p>Create the initial tables in PostgreSQL like so:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>CREATE</span><span> </span><span>DATABASE</span><span> </span><span>regression</span><span>;</span></div></div><div><div><span>\c regression</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <div><figure><figcaption></figcaption><pre><code><div><div><span>CREATE</span><span> </span><span>TABLE</span><span> </span><span>item</span><span> (</span></div></div><div><div><span><span> </span></span><span>item_id </span><span>SERIAL</span><span> </span><span>PRIMARY KEY</span><span>,</span></div></div><div><div><span> </span><span>name</span><span> </span><span>TEXT</span><span> </span><span>NOT NULL</span><span>,</span></div></div><div><div><span><span> </span></span><span>price </span><span>DECIMAL</span><span>(</span><span>10</span><span>, </span><span>2</span><span>) </span><span>NOT NULL</span><span> </span><span>DEFAULT</span><span> </span><span>0</span><span>.</span><span>00</span><span> </span><span>CHECK</span><span> (price </span><span>>=</span><span> </span><span>0</span><span>.</span><span>00</span><span>),</span></div></div><div><div><span><span> </span></span><span>quantity_on_hand </span><span>INTEGER</span><span> </span><span>NOT NULL</span><span> </span><span>DEFAULT</span><span> </span><span>0</span><span> </span><span>CHECK</span><span> (quantity_on_hand </span><span>>=</span><span> </span><span>0</span><span>),</span></div></div><div><div><span><span> </span></span><span>created_at </span><span>TIMESTAMPTZ</span><span> </span><span>DEFAULT</span><span> </span><span>NOW</span><span>(),</span></div></div><div><div><span><span> </span></span><span>updated_at </span><span>TIMESTAMPTZ</span><span> </span><span>DEFAULT</span><span> </span><span>NOW</span><span>()</span></div></div><div><div><span>);</span></div></div><div><div> </div></div><div><div><span>CREATE</span><span> </span><span>TABLE</span><span> "</span><span>order</span><span>" (</span></div></div><div><div><span><span> </span></span><span>order_id </span><span>SERIAL</span><span> </span><span>PRIMARY KEY</span><span>,</span></div></div><div><div><span> </span><span>status</span><span> </span><span>TEXT</span><span> </span><span>DEFAULT</span><span> </span><span>'</span><span>PENDING</span><span>'</span><span>,</span></div></div><div><div><span><span> </span></span><span>created_at </span><span>TIMESTAMPTZ</span><span> </span><span>DEFAULT</span><span> </span><span>NOW</span><span>(),</span></div></div><div><div><span><span> </span></span><span>updated_at </span><span>TIMESTAMPTZ</span><span> </span><span>DEFAULT</span><span> </span><span>NOW</span><span>()</span></div></div><div><div><span>);</span></div></div><div><div> </div></div><div><div><span>CREATE</span><span> </span><span>TABLE</span><span> </span><span>order_item</span><span> (</span></div></div><div><div><span><span> </span></span><span>order_item_id </span><span>SERIAL</span><span> </span><span>PRIMARY KEY</span><span>,</span></div></div><div><div><span><span> </span></span><span>order_id_order </span><span>INTEGER</span><span> </span><span>NOT NULL</span><span> </span><span>REFERENCES</span><span> </span><span>"</span><span>order</span><span>"</span><span>(order_id), </span><span>-- foreign key name is &#x3C;field>_&#x3C;table></span></div></div><div><div><span><span> </span></span><span>item_id_item </span><span>INTEGER</span><span> </span><span>NOT NULL</span><span> </span><span>REFERENCES</span><span> item(item_id),</span></div></div><div><div><span><span> </span></span><span>quantity </span><span>INTEGER</span><span> </span><span>NOT NULL</span><span> </span><span>CHECK</span><span> (quantity </span><span>></span><span> </span><span>0</span><span>),</span></div></div><div><div><span><span> </span></span><span>price_per_unit </span><span>DECIMAL</span><span>(</span><span>10</span><span>, </span><span>2</span><span>) </span><span>NOT NULL</span><span>,</span></div></div><div><div><span><span> </span></span><span>created_at </span><span>TIMESTAMPTZ</span><span> </span><span>DEFAULT</span><span> </span><span>NOW</span><span>(),</span></div></div><div><div><span><span> </span></span><span>updated_at </span><span>TIMESTAMPTZ</span><span> </span><span>DEFAULT</span><span> </span><span>NOW</span><span>()</span></div></div><div><div><span>);</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <div><h2 id="build-our-first-test">Build our first test</h2></div> <p>Let’s create our first test:</p> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>spawn</span><span> </span><span>test</span><span> </span><span>new</span><span> </span><span>check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>This will have created a very basic test inside <code dir="auto">tests/check-order-creation/test.sql</code>, with content similar to the following:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>-- Test file</span></div></div><div><div><span>SELECT</span><span> </span><span>1</span><span>;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Spawn executes your test file through psql, captures the textual output, and compares it against an expected “golden” file. <code dir="auto">spawn test expect</code> records the current output, while <code dir="auto">spawn test compare</code> reruns the SQL and diffs any changes to the expected output. To see what output this test would produce:</p> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>spawn</span><span> </span><span>test</span><span> </span><span>run</span><span> </span><span>check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>You should see output similar to the following:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>?column?</span></div></div><div><div><span>----------</span></div></div><div><div><span><span> </span></span><span>1</span></div></div><div><div><span>(1 row)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Imagine we were satisfied with this as our test. We can then tell Spawn that the output it currently generates is the expected output, and then we can run the actual test comparison:</p> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span># Creates a file `tests/check-order-creation/expected`</span></div></div><div><div><span>spawn</span><span> </span><span>test</span><span> </span><span>expect</span><span> </span><span>check-order-creation</span></div></div><div><div><span># Compares the output from running the test with the expected output:</span></div></div><div><div><span>spawn</span><span> </span><span>test</span><span> </span><span>compare</span><span> </span><span>check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Now, change the test from <code dir="auto">SELECT 1</code> to <code dir="auto">SELECT 2</code> and run compare again:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>spawn test compare check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>The test will fail, because the <code dir="auto">expected</code> file differs from the actual output. We see a report like this (which may be nicely coloured in your terminal):</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>[FAIL] check-order-creation</span></div></div><div><div><span>--- Diff ---</span></div></div><div><div><span>1 1 | ?column?</span></div></div><div><div><span>2 2 | ----------</span></div></div><div><div><span>3 |- 1</span></div></div><div><div><span><span> </span></span><span>3 |+ 2</span></div></div><div><div><span>4 4 | (1 row)</span></div></div><div><div><span>5 5 |</span></div></div><div><div> </div></div><div><div><span>-------------</span></div></div><div><div> </div></div><div><div><span>Error: ! Differences found in one or more tests</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>From the diff, we can see we expected a result of <code dir="auto">1</code>, but got <code dir="auto">2</code>.</p> <aside aria-label="Tip"> <p aria-hidden="true"> Tip </p> <div> <p>For ideas on how to handle non-deterministic output in tests, visit <a href="https://docs.spawn.dev/recipes/non-determinism-tests">Non-determinism in Tests</a>.</p> </div> </aside> <p>Let’s make a real test. First order of business, we want to create a copy of the database so that we can rerun the test multiple times without making permanent changes to our database. We <em>could</em> wrap the test in a transaction and roll it back (Spawn works fine this way), but some operations can’t run inside a transaction block (like creating databases), and sometimes you want to test behavior across commits. Using <code dir="auto">WITH TEMPLATE</code> gives you a clean slate every time without relying on rollback.</p> <p>Using the following pattern, we use the <code dir="auto">regression</code> database created as the base for tests, and create a copy of it using <code dir="auto">WITH TEMPLATE</code> within which we can freely make changes. As soon as the test is done, it cleans up the temporary test database and our changes are gone.</p> <aside aria-label="Transactions vs &#x60;WITH TEMPLATE&#x60;"> <p aria-hidden="true"> Transactions vs `WITH TEMPLATE` </p> <div> <p>You can write your tests however you please. You could use the base database and wrap everything in <code dir="auto">BEGIN</code> and <code dir="auto">ROLLBACK</code>, or you can use <code dir="auto">WITH TEMPLATE</code> to have a copy that you’re free to do anything in.</p> </div> </aside> <div><figure><figcaption></figcaption><pre><code><div><div><span>-- Protects from previous failed test</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div><div><div><span>-- Create new database using our base database as the template</span></div></div><div><div><span>CREATE</span><span> </span><span>DATABASE</span><span> </span><span>check_order_creation_test</span><span> </span><span>WITH</span><span> TEMPLATE regression;</span></div></div><div><div><span>-- Connect to the new test database using the psql \c command</span></div></div><div><div><span>\c check_order_creation_test;</span></div></div><div><div> </div></div><div><div><span>-- Insert an item and list what we have</span></div></div><div><div><span>INSERT INTO</span><span> item (</span><span>name</span><span>, quantity_on_hand, price) </span><span>VALUES</span><span> (</span><span>'</span><span>Apple</span><span>'</span><span>, </span><span>1</span><span>, </span><span>23</span><span>.</span><span>12</span><span>);</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, price </span><span>FROM</span><span> item </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>-- Connect back to postgres, so we can delete the test db</span></div></div><div><div><span>\c postgres;</span></div></div><div><div><span>-- Clean up!</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Every run of the test is done within its own copy of the base database. If we run the test multiple times (<code dir="auto">spawn test run check-order-creation</code>), the count doesn’t change, despite adding a row each time, because each time the test runs it starts from the same base state.</p> <aside aria-label="Caution"> <p aria-hidden="true"> Caution </p> <div> <p>You can only use <code dir="auto">WITH TEMPLATE</code> if there are no connections to the base database that you are copying. If you have a psql session open, you may need to connect back to <code dir="auto">\c postgres</code> to allow this to work.</p> </div> </aside> <p>But it’s just a little tedious to create items by hand, so let’s create a macro to simplify adding items to the database. Spawn uses <a href="https://docs.rs/minijinja/latest/minijinja/">minijinja</a> for templates, with more details and examples available in the <a href="https://docs.spawn.dev/reference/templating/">Spawn template docs</a>. Create a file in <code dir="auto">spawn/components/testing/create-item.sql</code> like so:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>{% macro create_item(</span></div></div><div><div><span> </span><span>name</span><span>,</span></div></div><div><div><span><span> </span></span><span>item_id</span><span>=</span><span>"</span><span>default</span><span>"</span><span> | </span><span>safe</span><span>,</span></div></div><div><div><span><span> </span></span><span>quantity_on_hand</span><span>=</span><span>1</span><span>,</span></div></div><div><div><span><span> </span></span><span>price</span><span>=</span><span>1</span><span>.</span><span>23</span><span>,</span></div></div><div><div><span>) %}</span></div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>({{item_id}}, {{</span><span>name</span><span>}}, {{quantity_on_hand}}, {{price}});</span></div></div><div><div> </div></div><div><div><span>{%</span><span>-</span><span> endmacro %}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>This macro creates the insert, with default values for everything except name. Note that for the primary key we have <code dir="auto">item_id="default" | safe</code>. If we provide an item id it uses that, but if we don’t provide one then the default value is used. We must pass <code dir="auto">"default"</code> through safe because Spawn defaults to escaping any input values as literals, but we want <code dir="auto">default</code> to appear without quote marks. i.e., to appear as <code dir="auto">default</code> and not <code dir="auto">"default"</code>. Using the <code dir="auto">safe</code> filter tells Spawn to display this as it is.</p> <p>Now in our original script, we can replace the insert with a single call to the macro:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>{% </span><span>from</span><span> </span><span>"</span><span>testing/create-item.sql</span><span>"</span><span> import create_item %}</span></div></div><div><div><span>...</span></div></div><div><div><span>{{ create_item(</span><span>'</span><span>Apple</span><span>'</span><span>, price</span><span>=</span><span>23</span><span>.</span><span>12</span><span>) }}</span></div></div><div><div><span>{{ create_item(</span><span>'</span><span>Banana</span><span>'</span><span>, price</span><span>=</span><span>44</span><span>.</span><span>00</span><span>, quantity_on_hand</span><span>=</span><span>5</span><span>) }}</span></div></div><div><div><span>{{ create_item(</span><span>'</span><span>Orange</span><span>'</span><span>, price</span><span>=</span><span>12</span><span>.</span><span>99</span><span>, quantity_on_hand</span><span>=</span><span>3</span><span>) }}</span></div></div><div><div><span>{{ create_item(</span><span>"</span><span>Dragon's Eye</span><span>"</span><span>, price</span><span>=</span><span>2</span><span>.</span><span>99</span><span>, quantity_on_hand</span><span>=</span><span>3</span><span>) }}</span></div></div><div><div><span>...</span></div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, price </span><span>FROM</span><span> item </span><span>ORDER BY</span><span> item_id;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>The macros produce the <code dir="auto">INSERT</code> SQL for us, which you can see by running <code dir="auto">spawn test build check-order-creation</code>:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div><div><div><span>CREATE</span><span> </span><span>DATABASE</span><span> </span><span>check_order_creation_test</span><span> </span><span>WITH</span><span> TEMPLATE regression;</span></div></div><div><div><span>\c check_order_creation_test;</span></div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>default</span><span>, </span><span>'</span><span>Apple</span><span>'</span><span>, </span><span>1</span><span>, </span><span>23</span><span>.</span><span>12</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>default</span><span>, </span><span>'</span><span>Banana</span><span>'</span><span>, </span><span>5</span><span>, </span><span>44</span><span>.</span><span>0</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>default</span><span>, </span><span>'</span><span>Orange</span><span>'</span><span>, </span><span>3</span><span>, </span><span>12</span><span>.</span><span>99</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>default</span><span>, </span><span>'</span><span>Dragon</span><span>''</span><span>s Eye</span><span>'</span><span>, </span><span>3</span><span>, </span><span>2</span><span>.</span><span>99</span><span>);</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, price </span><span>FROM</span><span> item </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>\c postgres;</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>And <code dir="auto">spawn test run check-order-creation</code>:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span><span> </span></span><span>item_id | name | price</span></div></div><div><div><span>---------+--------------+-------</span></div></div><div><div><span><span> </span></span><span>1 | Apple | 23.12</span></div></div><div><div><span><span> </span></span><span>2 | Banana | 44.00</span></div></div><div><div><span><span> </span></span><span>3 | Orange | 12.99</span></div></div><div><div><span><span> </span></span><span>4 | Dragon's Eye | 2.99</span></div></div><div><div><span>(4 rows)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Now, we might find ourselves wanting to use this same dataset across multiple tests, so let’s do two things:</p> <ol> <li>Create this list based on a json input</li> <li>Create a macro that fills out this table for us in one go</li> </ol> <p>In a real project, you might already have test data in the database you used as the base for <code dir="auto">WITH TEMPLATE</code>, but we’ll use this as an example to show how you could have some data that’s used by some tests but not all.</p> <p>Create a json file in <code dir="auto">spawn/components/testing/fixtures/items.json</code>:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>[</span></div></div><div><div><span><span> </span></span><span>{</span></div></div><div><div><span> </span><span>"item_id"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><span> </span><span>"name"</span><span>: </span><span>"</span><span>Apple</span><span>"</span><span>,</span></div></div><div><div><span> </span><span>"quantity_on_hand"</span><span>: </span><span>1</span><span>,</span></div></div><div><div><span> </span><span>"price"</span><span>: </span><span>23.12</span></div></div><div><div><span><span> </span></span><span>},</span></div></div><div><div><span><span> </span></span><span>{</span></div></div><div><div><span> </span><span>"item_id"</span><span>: </span><span>2</span><span>,</span></div></div><div><div><span> </span><span>"name"</span><span>: </span><span>"</span><span>Banana</span><span>"</span><span>,</span></div></div><div><div><span> </span><span>"quantity_on_hand"</span><span>: </span><span>5</span><span>,</span></div></div><div><div><span> </span><span>"price"</span><span>: </span><span>44.0</span></div></div><div><div><span><span> </span></span><span>},</span></div></div><div><div><span><span> </span></span><span>{</span></div></div><div><div><span> </span><span>"item_id"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><span> </span><span>"name"</span><span>: </span><span>"</span><span>Orange</span><span>"</span><span>,</span></div></div><div><div><span> </span><span>"quantity_on_hand"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><span> </span><span>"price"</span><span>: </span><span>12.99</span></div></div><div><div><span><span> </span></span><span>},</span></div></div><div><div><span><span> </span></span><span>{</span></div></div><div><div><span> </span><span>"item_id"</span><span>: </span><span>4</span><span>,</span></div></div><div><div><span> </span><span>"name"</span><span>: </span><span>"</span><span>Dragon's Eye</span><span>"</span><span>,</span></div></div><div><div><span> </span><span>"quantity_on_hand"</span><span>: </span><span>3</span><span>,</span></div></div><div><div><span> </span><span>"price"</span><span>: </span><span>2.99</span></div></div><div><div><span><span> </span></span><span>}</span></div></div><div><div><span>]</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <aside aria-label="Tip"> <p aria-hidden="true"> Tip </p> <div> <p>Spawn escapes values like strings automatically as literals. Visit <a href="https://docs.spawn.dev/reference/templating/#sql-escaping-and-security">SQL escaping and security</a> for more information.</p> </div> </aside> <p>Let’s also create a new macro which is going to read this json, and loop over the items to create all our rows! Let’s put it in <code dir="auto">spawn/components/testing/fixtures/items.sql</code> (we don’t have to use the <code dir="auto">.sql</code> extension here, but for consistency I have):</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>{% </span><span>from</span><span> </span><span>"</span><span>testing/create-item.sql</span><span>"</span><span> import create_item %}</span></div></div><div><div> </div></div><div><div><span>{% macro create_items() %}</span></div></div><div><div><span>{% </span><span>set</span><span> items </span><span>=</span><span> </span><span>"</span><span>testing/fixtures/items.json</span><span>"</span><span> | read_json %}</span></div></div><div><div><span>{% </span><span>for</span><span> item </span><span>in</span><span> items %}</span></div></div><div><div><span><span> </span></span><span>{{ create_item(</span><span>item</span><span>.</span><span>name</span><span>, item_id</span><span>=</span><span>item</span><span>.</span><span>item_id</span><span>, quantity_on_hand</span><span>=</span><span>item</span><span>.</span><span>quantity_on_hand</span><span>, price</span><span>=</span><span>item</span><span>.</span><span>price</span><span>) }}</span></div></div><div><div><span>{% endfor %}</span></div></div><div><div><span>{% endmacro %}</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>This macro loads the array from <code dir="auto">items.json</code>, loops over each, and calls our earlier <code dir="auto">create_item</code> macro to create the insert statement for each!</p> <p>Update our test file <code dir="auto">test.sql</code> to use the new macro, like so:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>{% </span><span>from</span><span> </span><span>"</span><span>testing/fixtures/items.sql</span><span>"</span><span> import create_items %}</span></div></div><div><div> </div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div><div><div><span>CREATE</span><span> </span><span>DATABASE</span><span> </span><span>check_order_creation_test</span><span> </span><span>WITH</span><span> TEMPLATE regression;</span></div></div><div><div><span>\c check_order_creation_test;</span></div></div><div><div> </div></div><div><div><span>{{ create_items() }}</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, price </span><span>FROM</span><span> item </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>\c postgres;</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>And let’s build and then run. First, build shows us that the apostrophe in <code dir="auto">Dragon's Eye</code> is properly escaped:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div><div><div><span>CREATE</span><span> </span><span>DATABASE</span><span> </span><span>check_order_creation_test</span><span> </span><span>WITH</span><span> TEMPLATE regression;</span></div></div><div><div><span>\c check_order_creation_test;</span></div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>1</span><span>, </span><span>'</span><span>Apple</span><span>'</span><span>, </span><span>1</span><span>, </span><span>23</span><span>.</span><span>12</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>2</span><span>, </span><span>'</span><span>Banana</span><span>'</span><span>, </span><span>5</span><span>, </span><span>44</span><span>.</span><span>0</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>3</span><span>, </span><span>'</span><span>Orange</span><span>'</span><span>, </span><span>3</span><span>, </span><span>12</span><span>.</span><span>99</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div><span>INSERT INTO</span><span> item (item_id, </span><span>name</span><span>, quantity_on_hand, price)</span></div></div><div><div><span>VALUES</span></div></div><div><div><span><span> </span></span><span>(</span><span>4</span><span>, </span><span>'</span><span>Dragon</span><span>''</span><span>s Eye</span><span>'</span><span>, </span><span>3</span><span>, </span><span>2</span><span>.</span><span>99</span><span>);</span></div></div><div><div> </div></div><div><div> </div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, price </span><span>FROM</span><span> item </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>\c postgres;</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>And the output for the test shows the four items:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span><span> </span></span><span>item_id | name | price</span></div></div><div><div><span>---------+--------------+-------</span></div></div><div><div><span><span> </span></span><span>1 | Apple | 23.12</span></div></div><div><div><span><span> </span></span><span>2 | Banana | 44.00</span></div></div><div><div><span><span> </span></span><span>3 | Orange | 12.99</span></div></div><div><div><span><span> </span></span><span>4 | Dragon's Eye | 2.99</span></div></div><div><div><span>(4 rows)</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Now we have an easy to include test fixture of items!</p> <div><h2 id="creating-an-order">Creating an order</h2></div> <p>To demonstrate how we can do some tests with functions and triggers, let’s create a function for creating an order, and a trigger that updates quantity on hand when orders are created, updated, or deleted. First, let’s create a function for creating an order, and apply it to our <code dir="auto">regression</code> base database:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>-- Composite type for order item input</span></div></div><div><div><span>CREATE</span><span> </span><span>TYPE</span><span> </span><span>order_item_input</span><span> </span><span>AS</span><span> (</span></div></div><div><div><span><span> </span></span><span>quantity </span><span>INTEGER</span><span>,</span></div></div><div><div><span><span> </span></span><span>item_id </span><span>INTEGER</span></div></div><div><div><span>);</span></div></div><div><div> </div></div><div><div><span>-- Function to create an order with items</span></div></div><div><div><span>CREATE OR REPLACE</span><span> </span><span>FUNCTION</span><span> </span><span>create_order</span><span>(</span></div></div><div><div><span><span> </span></span><span>p_status </span><span>TEXT</span><span>,</span></div></div><div><div><span><span> </span></span><span>p_items order_item_input[]</span></div></div><div><div><span>) </span><span>RETURNS</span><span> </span><span>INTEGER</span><span> </span><span>AS</span><span> $$</span></div></div><div><div><span>DECLARE</span></div></div><div><div><span><span> </span></span><span>v_order_id </span><span>INTEGER</span><span>;</span></div></div><div><div><span>BEGIN</span></div></div><div><div><span> </span><span>-- Create the order</span></div></div><div><div><span> </span><span>INSERT INTO</span><span> </span><span>"</span><span>order</span><span>"</span><span> (</span><span>status</span><span>)</span></div></div><div><div><span> </span><span>VALUES</span><span> (p_status)</span></div></div><div><div><span><span> </span></span><span>RETURNING order_id </span><span>INTO</span><span> v_order_id;</span></div></div><div><div> </div></div><div><div><span> </span><span>-- Create order items with prices from item table</span></div></div><div><div><span> </span><span>INSERT INTO</span><span> order_item (order_id_order, item_id_item, quantity, price_per_unit)</span></div></div><div><div><span> </span><span>SELECT</span><span> v_order_id, </span><span>items</span><span>.</span><span>item_id</span><span>, </span><span>items</span><span>.</span><span>quantity</span><span>, </span><span>i</span><span>.</span><span>price</span></div></div><div><div><span> </span><span>FROM</span><span> UNNEST(p_items) </span><span>AS</span><span> items</span></div></div><div><div><span> </span><span>LEFT JOIN</span><span> item i </span><span>ON</span><span> </span><span>i</span><span>.</span><span>item_id</span><span> </span><span>=</span><span> </span><span>items</span><span>.</span><span>item_id</span><span>; </span><span>-- missing items produce NULL price and fail NOT NULL on price_per_unit</span></div></div><div><div><span> </span><span>RETURN</span><span> v_order_id;</span></div></div><div><div><span>END</span><span>;</span></div></div><div><div><span>$$ </span><span>LANGUAGE</span><span> plpgsql;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Now we’ll create a trigger function and trigger to automatically update inventory when order items change. This is not a way that I would recommend building an order platform, but it’s useful from the perspective of showing how easy it is to test triggers and functions with Spawn. This trigger updates the <code dir="auto">quantity_on_hand</code> column in the <code dir="auto">item</code> table when an order item is updated. And since the <code dir="auto">item</code> table has a <code dir="auto">CHECK</code> constraint that prohibits values lower than 0, we will get an error when a change would result in dropping our stock below 0. Create this in the <code dir="auto">regression</code> base database:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>-- Trigger function to update item quantity on hand</span></div></div><div><div><span>CREATE OR REPLACE</span><span> </span><span>FUNCTION</span><span> </span><span>update_item_quantity_on_order_item_change</span><span>()</span></div></div><div><div><span>RETURNS</span><span> TRIGGER </span><span>AS</span><span> $$</span></div></div><div><div><span>BEGIN</span></div></div><div><div><span> </span><span>UPDATE</span><span> item</span></div></div><div><div><span> </span><span>SET</span><span> quantity_on_hand </span><span>=</span><span> quantity_on_hand </span><span>+</span><span> </span><span>COALESCE</span><span>(</span><span>OLD</span><span>.</span><span>quantity</span><span>, </span><span>0</span><span>) </span><span>-</span><span> </span><span>COALESCE</span><span>(</span><span>NEW</span><span>.</span><span>quantity</span><span>, </span><span>0</span><span>)</span></div></div><div><div><span> </span><span>WHERE</span><span> item_id </span><span>=</span><span> </span><span>COALESCE</span><span>(</span><span>NEW</span><span>.</span><span>item_id_item</span><span>, </span><span>OLD</span><span>.</span><span>item_id_item</span><span>);</span></div></div><div><div> </div></div><div><div><span> </span><span>RETURN</span><span> </span><span>COALESCE</span><span>(NEW, OLD);</span></div></div><div><div><span>END</span><span>;</span></div></div><div><div><span>$$ </span><span>LANGUAGE</span><span> plpgsql;</span></div></div><div><div> </div></div><div><div><span>-- Create the trigger on our order_item table</span></div></div><div><div><span>CREATE</span><span> </span><span>TRIGGER</span><span> </span><span>order_item_quantity_trigger</span></div></div><div><div><span> </span><span>AFTER</span><span> </span><span>INSERT</span><span> </span><span>OR</span><span> </span><span>UPDATE</span><span> </span><span>OR</span><span> </span><span>DELETE</span><span> </span><span>ON</span><span> order_item</span></div></div><div><div><span> </span><span>FOR</span><span> EACH </span><span>ROW</span></div></div><div><div><span> </span><span>EXECUTE</span><span> </span><span>FUNCTION</span><span> update_item_quantity_on_order_item_change();</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>And now we can update our test to create orders. Let’s try to create two orders, the first of which we expect to succeed, and the second to fail. I’ve also included some notes to help with understanding the tests in the future, as well as selecting items from the <code dir="auto">item</code> table as we go, to see how stock levels change over time:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>{% </span><span>from</span><span> </span><span>"</span><span>testing/fixtures/items.sql</span><span>"</span><span> import create_items %}</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div><div><div><span>CREATE</span><span> </span><span>DATABASE</span><span> </span><span>check_order_creation_test</span><span> </span><span>WITH</span><span> TEMPLATE regression;</span></div></div><div><div><span>\c check_order_creation_test;</span></div></div><div><div><span>-- By default, spawn stops on errors, but we want errors to be part of our test:</span></div></div><div><div><span>\</span><span>set</span><span> ON_ERROR_STOP </span><span>off</span></div></div><div><div><span>-- The default order includes timestamps. By using a terse verbosity, we get a stable error across runs:</span></div></div><div><div><span>\</span><span>set</span><span> VERBOSITY terse</span></div></div><div><div> </div></div><div><div><span>{{ create_items() }}</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, quantity_on_hand, price </span><span>FROM</span><span> item </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>BEGIN</span><span>;</span></div></div><div><div><span>SELECT</span><span> </span><span>'</span><span>Create order for 1 apple and two bananas, reducing quantity on hand:</span><span>'</span><span> </span><span>as</span><span> note;</span></div></div><div><div><span>-- gset stores the returned value under :'first_order_id'</span></div></div><div><div><span>SELECT</span><span> create_order(</span><span>'</span><span>PENDING</span><span>'</span><span>, </span><span>ARRAY</span><span>[</span></div></div><div><div><span> </span><span>ROW</span><span>(</span><span>1</span><span>, </span><span>1</span><span>)::order_item_input, </span><span>-- 1 Apple (item_id 1)</span></div></div><div><div><span> </span><span>ROW</span><span>(</span><span>2</span><span>, </span><span>2</span><span>)::order_item_input </span><span>-- 2 Bananas (item_id 2)</span></div></div><div><div><span>]) </span><span>as</span><span> first_order_id \gset</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, quantity_on_hand, price </span><span>FROM</span><span> item </span><span>WHERE</span><span> item_id </span><span>IN</span><span> (</span><span>1</span><span>, </span><span>2</span><span>) </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> </span><span>'</span><span>Increased banana order by one, expect one less on hand:</span><span>'</span><span> </span><span>as</span><span> note;</span></div></div><div><div> </div></div><div><div><span>UPDATE</span><span> order_item </span><span>SET</span><span> quantity </span><span>=</span><span> quantity </span><span>+</span><span> </span><span>1</span><span> </span><span>WHERE</span><span> item_id_item </span><span>=</span><span> </span><span>2</span><span> </span><span>AND</span><span> order_id_order </span><span>=</span><span> :</span><span>'</span><span>first_order_id</span><span>'</span><span>;</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, quantity_on_hand, price </span><span>FROM</span><span> item </span><span>WHERE</span><span> item_id </span><span>IN</span><span> (</span><span>1</span><span>, </span><span>2</span><span>) </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> </span><span>'</span><span>Delete bananas from order, expecting all to be back on hand:</span><span>'</span><span> </span><span>as</span><span> note;</span></div></div><div><div> </div></div><div><div><span>DELETE</span><span> </span><span>FROM</span><span> order_item </span><span>WHERE</span><span> item_id_item </span><span>=</span><span> </span><span>2</span><span> </span><span>AND</span><span> order_id_order </span><span>=</span><span> :</span><span>'</span><span>first_order_id</span><span>'</span><span>;</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> item_id, </span><span>name</span><span>, quantity_on_hand, price </span><span>FROM</span><span> item </span><span>WHERE</span><span> item_id </span><span>IN</span><span> (</span><span>1</span><span>, </span><span>2</span><span>) </span><span>ORDER BY</span><span> item_id;</span></div></div><div><div> </div></div><div><div><span>SELECT</span><span> </span><span>'</span><span>New order for 1 apple and 2 bananas which should fail due to apple shortage:</span><span>'</span><span> </span><span>as</span><span> note;</span></div></div><div><div><span>SELECT</span><span> create_order(</span><span>'</span><span>PENDING</span><span>'</span><span>, </span><span>ARRAY</span><span>[</span></div></div><div><div><span> </span><span>ROW</span><span>(</span><span>1</span><span>, </span><span>1</span><span>)::order_item_input, </span><span>-- Apple (item_id 1)</span></div></div><div><div><span> </span><span>ROW</span><span>(</span><span>2</span><span>, </span><span>2</span><span>)::order_item_input </span><span>-- 2 Bananas (item_id 2)</span></div></div><div><div><span>]);</span></div></div><div><div><span>ROLLBACK</span><span>;</span></div></div><div><div> </div></div><div><div><span>\c postgres;</span></div></div><div><div><span>DROP</span><span> </span><span>DATABASE</span><span> </span><span>IF</span><span> </span><span>EXISTS</span><span> check_order_creation_test;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>We expect the first order to succeed, and the second to fail, and that’s exactly what we see (<code dir="auto">spawn test run check-order-creation</code>):</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>NOTICE: database "check_order_creation_test" does not exist, skipping</span></div></div><div><div><span><span> </span></span><span>item_id | name | quantity_on_hand | price</span></div></div><div><div><span>---------+--------------+------------------+-------</span></div></div><div><div><span><span> </span></span><span>1 | Apple | 1 | 23.12</span></div></div><div><div><span><span> </span></span><span>2 | Banana | 5 | 44.00</span></div></div><div><div><span><span> </span></span><span>3 | Orange | 3 | 12.99</span></div></div><div><div><span><span> </span></span><span>4 | Dragon's Eye | 3 | 2.99</span></div></div><div><div><span>(4 rows)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>note</span></div></div><div><div><span>----------------------------------------------------------------------</span></div></div><div><div><span><span> </span></span><span>Create order for 1 apple and two bananas, reducing quantity on hand:</span></div></div><div><div><span>(1 row)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>item_id | name | quantity_on_hand | price</span></div></div><div><div><span>---------+--------+------------------+-------</span></div></div><div><div><span><span> </span></span><span>1 | Apple | 0 | 23.12</span></div></div><div><div><span><span> </span></span><span>2 | Banana | 3 | 44.00</span></div></div><div><div><span>(2 rows)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>note</span></div></div><div><div><span>---------------------------------------------------------</span></div></div><div><div><span><span> </span></span><span>Increased banana order by one, expect one less on hand:</span></div></div><div><div><span>(1 row)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>item_id | name | quantity_on_hand | price</span></div></div><div><div><span>---------+--------+------------------+-------</span></div></div><div><div><span><span> </span></span><span>1 | Apple | 0 | 23.12</span></div></div><div><div><span><span> </span></span><span>2 | Banana | 2 | 44.00</span></div></div><div><div><span>(2 rows)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>note</span></div></div><div><div><span>--------------------------------------------------------------</span></div></div><div><div><span><span> </span></span><span>Delete bananas from order, expecting all to be back on hand:</span></div></div><div><div><span>(1 row)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>item_id | name | quantity_on_hand | price</span></div></div><div><div><span>---------+--------+------------------+-------</span></div></div><div><div><span><span> </span></span><span>1 | Apple | 0 | 23.12</span></div></div><div><div><span><span> </span></span><span>2 | Banana | 5 | 44.00</span></div></div><div><div><span>(2 rows)</span></div></div><div><div> </div></div><div><div><span><span> </span></span><span>note</span></div></div><div><div><span>------------------------------------------------------------------------------</span></div></div><div><div><span><span> </span></span><span>New order for 1 apple and 2 bananas which should fail due to apple shortage:</span></div></div><div><div><span>(1 row)</span></div></div><div><div> </div></div><div><div><span>ERROR: new row for relation "item" violates check constraint "item_quantity_on_hand_check"</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>That’s finished! Our test validates that basic use of the order works as expected, and that the trigger updates the underlying table in the expected way. We’ve included some notes to make things easier for future testers to understand what is going on. Let’s set the current output as the expected output for our test, and run it:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>spawn test expect check-order-creation</span></div></div><div><div><span>spawn test compare check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>[PASS] check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>And one final validation: connect to the database, and drop the trigger from the table and rerun compare, just to see the output:</p> <div><figure><figcaption></figcaption><pre><code><div><div><span>DROP</span><span> </span><span>TRIGGER</span><span> order_item_quantity_trigger </span><span>ON</span><span> order_item;</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <div><figure><figcaption></figcaption><pre><code><div><div><span>spawn test compare check-order-creation</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <div><figure><figcaption><span></span></figcaption><pre><code><div><div><span>[FAIL] check-order-creation</span></div></div><div><div><span>---</span><span> </span><span>Diff</span><span> </span><span>---</span></div></div><div><div><span>14</span><span> </span><span>14</span><span> </span><span>|</span></div></div><div><div><span>15</span><span> </span><span>15</span><span> </span><span>|</span><span> </span><span>item_id</span><span> </span><span>|</span><span> </span><span>name</span><span> </span><span>|</span><span> </span><span>quantity_on_hand</span><span> </span><span>|</span><span> </span><span>price</span></div></div><div><div><span>16</span><span> </span><span>16</span><span> </span><span>|</span><span> </span><span>---------+--------+------------------+-------</span></div></div><div><div><span>17</span><span> </span><span>|</span><span>-</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>Apple</span><span> </span><span>|</span><span> </span><span>0</span><span> </span><span>|</span><span> </span><span>23.12</span></div></div><div><div><span>18</span><span> </span><span>|</span><span>-</span><span> </span><span>2</span><span> </span><span>|</span><span> </span><span>Banana</span><span> </span><span>|</span><span> </span><span>3</span><span> </span><span>|</span><span> </span><span>44.00</span></div></div><div><div><span> </span><span>17</span><span> </span><span>|</span><span>+</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>Apple</span><span> </span><span>|</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>23.12</span></div></div><div><div><span> </span><span>18</span><span> </span><span>|</span><span>+</span><span> </span><span>2</span><span> </span><span>|</span><span> </span><span>Banana</span><span> </span><span>|</span><span> </span><span>5</span><span> </span><span>|</span><span> </span><span>44.00</span></div></div><div><div><span>19</span><span> </span><span>19</span><span> </span><span>|</span><span> (</span><span>2</span><span> </span><span>rows</span><span>)</span></div></div><div><div><span>20</span><span> </span><span>20</span><span> </span><span>|</span></div></div><div><div><span>21</span><span> </span><span>21</span><span> </span><span>|</span><span> </span><span>note</span></div></div><div><div><span>--------------------------------------------------------------------------------25</span><span> </span><span>25</span><span> </span><span>|</span></div></div><div><div><span>26</span><span> </span><span>26</span><span> </span><span>|</span><span> </span><span>item_id</span><span> </span><span>|</span><span> </span><span>name</span><span> </span><span>|</span><span> </span><span>quantity_on_hand</span><span> </span><span>|</span><span> </span><span>price</span></div></div><div><div><span>27</span><span> </span><span>27</span><span> </span><span>|</span><span> </span><span>---------+--------+------------------+-------</span></div></div><div><div><span>28</span><span> </span><span>|</span><span>-</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>Apple</span><span> </span><span>|</span><span> </span><span>0</span><span> </span><span>|</span><span> </span><span>23.12</span></div></div><div><div><span>29</span><span> </span><span>|</span><span>-</span><span> </span><span>2</span><span> </span><span>|</span><span> </span><span>Banana</span><span> </span><span>|</span><span> </span><span>2</span><span> </span><span>|</span><span> </span><span>44.00</span></div></div><div><div><span> </span><span>28</span><span> </span><span>|</span><span>+</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>Apple</span><span> </span><span>|</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>23.12</span></div></div><div><div><span> </span><span>29</span><span> </span><span>|</span><span>+</span><span> </span><span>2</span><span> </span><span>|</span><span> </span><span>Banana</span><span> </span><span>|</span><span> </span><span>5</span><span> </span><span>|</span><span> </span><span>44.00</span></div></div><div><div><span>30</span><span> </span><span>30</span><span> </span><span>|</span><span> (</span><span>2</span><span> </span><span>rows</span><span>)</span></div></div><div><div><span>31</span><span> </span><span>31</span><span> </span><span>|</span></div></div><div><div><span>32</span><span> </span><span>32</span><span> </span><span>|</span><span> </span><span>note</span></div></div><div><div><span>--------------------------------------------------------------------------------36</span><span> </span><span>36</span><span> </span><span>|</span></div></div><div><div><span>37</span><span> </span><span>37</span><span> </span><span>|</span><span> </span><span>item_id</span><span> </span><span>|</span><span> </span><span>name</span><span> </span><span>|</span><span> </span><span>quantity_on_hand</span><span> </span><span>|</span><span> </span><span>price</span></div></div><div><div><span>38</span><span> </span><span>38</span><span> </span><span>|</span><span> </span><span>---------+--------+------------------+-------</span></div></div><div><div><span>39</span><span> </span><span>|</span><span>-</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>Apple</span><span> </span><span>|</span><span> </span><span>0</span><span> </span><span>|</span><span> </span><span>23.12</span></div></div><div><div><span> </span><span>39</span><span> </span><span>|</span><span>+</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>Apple</span><span> </span><span>|</span><span> </span><span>1</span><span> </span><span>|</span><span> </span><span>23.12</span></div></div><div><div><span>40</span><span> </span><span>40</span><span> </span><span>|</span><span> </span><span>2</span><span> </span><span>|</span><span> </span><span>Banana</span><span> </span><span>|</span><span> </span><span>5</span><span> </span><span>|</span><span> </span><span>44.00</span></div></div><div><div><span>41</span><span> </span><span>41</span><span> </span><span>|</span><span> (</span><span>2</span><span> </span><span>rows</span><span>)</span></div></div><div><div><span>42</span><span> </span><span>42</span><span> </span><span>|</span></div></div><div><div><span>--------------------------------------------------------------------------------45</span><span> </span><span>45</span><span> </span><span>|</span><span> </span><span>New</span><span> </span><span>order</span><span> </span><span>for</span><span> </span><span>1</span><span> </span><span>apple</span><span> </span><span>and</span><span> </span><span>2</span><span> </span><span>bananas</span><span> </span><span>which</span><span> </span><span>should</span><span> </span><span>fail</span><span> </span><span>due</span><span> </span><span>to</span><span> </span><span>apple</span><span> </span><span>shortage:</span></div></div><div><div><span>46</span><span> </span><span>46</span><span> </span><span>|</span><span> (</span><span>1</span><span> </span><span>row</span><span>)</span></div></div><div><div><span>47</span><span> </span><span>47</span><span> </span><span>|</span></div></div><div><div><span>48</span><span> </span><span>|</span><span>-ERROR:</span><span> </span><span>new</span><span> </span><span>row</span><span> </span><span>for</span><span> </span><span>relation</span><span> </span><span>"</span><span>item</span><span>"</span><span> </span><span>violates</span><span> </span><span>check</span><span> </span><span>constraint</span><span> </span><span>"</span><span>item_quantity_on_hand_check</span><span>"</span></div></div><div><div><span> </span><span>48</span><span> </span><span>|</span><span>+</span><span> </span><span>create_order</span></div></div><div><div><span> </span><span>49</span><span> </span><span>|</span><span>+--------------</span></div></div><div><div><span> </span><span>50</span><span> </span><span>|</span><span>+</span><span> </span><span>4</span></div></div><div><div><span> </span><span>51</span><span> </span><span>|+</span><span>(</span><span>1</span><span> </span><span>row</span><span>)</span></div></div><div><div><span> </span><span>52</span><span> </span><span>|</span><span>+</span></div></div><div><div> </div></div><div><div><span>-------------</span></div></div><div><div> </div></div><div><div><span>Error:</span><span> </span><span>!</span><span> </span><span>Differences</span><span> </span><span>found</span><span> </span><span>in</span><span> </span><span>one</span><span> </span><span>or</span><span> </span><span>more</span><span> </span><span>tests</span></div></div></code></pre><div><div aria-live="polite"></div></div></figure></div> <p>Here it is in glorious colour:</p> <img src="https://docs.spawn.dev/_astro/example_diff.CSLX_qJx_BsMoV.webp" alt="Diff showing test failure with quantity differences highlighted" loading="lazy" decoding="async" fetchpriority="auto" width="1546" height="1374"> <p>Perfect! If the trigger is ever removed, our regression test will pick that up! It validates that the database behaves in the way we require.</p> <p>As your tests grow, you may want to explore using <code dir="auto">json</code> sources to loop over multiple test cases to create data driven tests. The ability to include external structured data (including binary data via <code dir="auto">read_file</code> and <code dir="auto">base64_encode</code>) opens up new possibilities for streamlining tests, and I’m sure there are many clever things that will be achievable that creative users will think of. Spawn is far from complete and as it is used more, new features will be added to support robust testing of your database.</p> <p>Hopefully this shows you a little idea of the possibilities for testing your PostgreSQL database using Spawn. There’s a short introduction covering both Spawn’s migration and testing capabilities in <a href="https://docs.spawn.dev/getting-started/magic/">The Magic of Spawn</a>, if you’d like to learn more about its migration features.</p> <p>Spawn is also able to be used as a GitHub action, which is documented at <a href="https://docs.spawn.dev/reference/ci-cd/">CI/CD</a>.</p>