<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Vibe Coding Forem: Niclas Olofsson</title>
    <description>The latest articles on Vibe Coding Forem by Niclas Olofsson (@niclasolofsson).</description>
    <link>https://vibe.forem.com/niclasolofsson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3409803%2F8d7ff3b3-a957-4889-9e2e-cabad1bd1fd1.jpeg</url>
      <title>Vibe Coding Forem: Niclas Olofsson</title>
      <link>https://vibe.forem.com/niclasolofsson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://vibe.forem.com/feed/niclasolofsson"/>
    <language>en</language>
    <item>
      <title>TDD for dbt: unit testing the way it should be</title>
      <dc:creator>Niclas Olofsson</dc:creator>
      <pubDate>Wed, 07 Jan 2026 00:00:53 +0000</pubDate>
      <link>https://vibe.forem.com/niclasolofsson/tdd-for-dbt-unit-testing-the-way-it-should-be-1l02</link>
      <guid>https://vibe.forem.com/niclasolofsson/tdd-for-dbt-unit-testing-the-way-it-should-be-1l02</guid>
      <description>&lt;p&gt;&lt;em&gt;Unit testing arrived in dbt 1.8. Finally, right? Except nobody does it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  About this article
  &lt;br&gt;
I wrote this. The ideas are mine; the execution is collaborative.&lt;br&gt;


&lt;/p&gt;




&lt;p&gt;Write tests with mock data, verify your business logic, practice TDD like any proper software engineer. That's what dbt 1.8 promised. Except nobody does it.&lt;/p&gt;

&lt;p&gt;And I get it. I tried. You sit down with good intentions, open a new YAML file, and then reality hits. You need to figure out which models your test depends on. Query the warehouse to get realistic sample data. Format everything as YAML dictionaries with the right structure. Then do it again for every edge case you want to cover. Then maintain all of it as your models evolve.&lt;/p&gt;

&lt;p&gt;That's not TDD. That's YAML accounting.&lt;/p&gt;

&lt;p&gt;So the feature sits there, unused. Which creates two problems that feed into each other.&lt;/p&gt;

&lt;p&gt;First, TDD stays theoretical. The boilerplate overhead kills any chance of test-first development. You write the model, then maybe add tests later if you have time. You don't.&lt;/p&gt;

&lt;p&gt;Second, and this is the one that hit me harder, your AI collaboration suffers. Without tests, there's no verification mechanism. You ask Copilot to implement something, it generates code, and now you're stuck manually checking if the logic is right. Query the warehouse, eyeball the results, spot the bug, explain it, wait for the fix, check again. Your flow state is gone. You're babysitting syntax instead of thinking about the problem.&lt;/p&gt;

&lt;p&gt;But here's what I discovered building dbt-core-mcp: when AI can handle the tedious parts, unit tests stop being a burden and start being a dialogue. The same tooling that lets AI scaffold YAML fixtures also lets AI iterate with test guardrails. Write test, run test, fix code, run test. That loop can happen without you leaving navigation mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refactoring problem
&lt;/h2&gt;

&lt;p&gt;Software developers figured this out decades ago. Martin Fowler documented it extensively: you can't maintain quality code without refactoring. And you can't refactor with confidence unless you have tests.&lt;/p&gt;

&lt;p&gt;You write code. It works. Six months later, requirements change or you understand the domain better or the model becomes too complex. You need to restructure it. Simplify the logic. Optimize the queries. Make it maintainable again.&lt;/p&gt;

&lt;p&gt;Without tests, refactoring is terrifying. Change the SQL, run the full pipeline, manually verify the output matches what it used to produce. Hope you didn't break something subtle. That fear keeps you from refactoring, so the code rots. Technical debt compounds.&lt;/p&gt;

&lt;p&gt;With tests, refactoring is mechanical. Change the implementation, run the tests, they pass, you're done. The tests document what the model should do. As long as the behavior stays consistent, the implementation can evolve.&lt;/p&gt;

&lt;p&gt;And here's what happens once you get used to it: you start finding the rhythm. Maybe you write tests after the fact at first, adding coverage to legacy models as you touch them. But eventually, you notice it's easier to write the test first. Define the edge case, write the test, then implement the logic that makes it pass. That's TDD. Test-driven development.&lt;/p&gt;

&lt;p&gt;Analytics engineering could have had this all along. The capability was there in dbt 1.8. But the YAML accounting killed adoption before it started. Writing fixtures by hand, sampling data, formatting dictionaries, it was too much friction.&lt;/p&gt;

&lt;p&gt;AI removes that barrier. The tedious parts get automated. Suddenly TDD isn't theoretical anymore. It's practical. And that changes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  When bugs happen
&lt;/h2&gt;

&lt;p&gt;Software developers learned another pattern: when something breaks, you don't just fix it. You write a failing test first.&lt;/p&gt;

&lt;p&gt;The workflow: a bug gets reported. Customers with exactly one order are showing null for first_order_date. Before touching the code, you write a test that reproduces the problem. One customer, one order, assert the date should match. Run it. It fails. Good - now you've proven you understand the bug.&lt;/p&gt;

&lt;p&gt;Then you fix the code. Maybe you forgot to handle the single-record case in your aggregation. Add the logic, run the test, it passes, ship it.&lt;/p&gt;

&lt;p&gt;But here's the real value: that test stays. Forever. It's not just a bug fix anymore, it's documentation. Six months from now when someone refactors that model, the test will catch it if they reintroduce the same bug. The test is evidence that this edge case matters, that it broke before, and here's exactly what the correct behavior should be.&lt;/p&gt;

&lt;p&gt;Without tests, bug fixes are "I think I fixed it, seems to work now, hope it doesn't come back." With tests, bug fixes are "Here's the test that proves it was broken, here's the test passing that proves it's fixed, and here's the permanent guard against regression."&lt;/p&gt;

&lt;p&gt;And yes, AI can help here too. "Write a test that reproduces the bug where customers with one order get null dates." Copilot scaffolds the failing test, you verify it actually fails for the right reason, then ask AI to fix it. Or fix it yourself. Either way, the test documents the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this actually looks like
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Without tests:&lt;/strong&gt; I ask Copilot to add customer order counts. It generates code. Now what? I need to verify it works. Maybe I run the model and query the output in Databricks. Maybe I just spot-check a few rows. Maybe I trust it and ship it. Whatever I do, it's manual and ad-hoc. No systematic verification. And when I notice it returns null for customers with no orders, we're back to the same cycle: point it out, wait for fix, check again. Context switching. Flow broken. Hope it's right this time.&lt;/p&gt;

&lt;p&gt;And let's be honest: "without tests" is today's default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With tests:&lt;/strong&gt; I say "Add customer order count, zero for customers with no orders, not null." Copilot inspects the model dependencies through dbt-core-mcp, samples some data, writes a unit test for the edge case, implements the model, runs the test, fails, fixes the coalesce, runs again, passes, and tells me "Test passing, ready for review."&lt;/p&gt;

&lt;p&gt;I review the test assertion and the implementation together. One cycle. I never left the conversation.&lt;/p&gt;

&lt;p&gt;The trick isn't that AI writes better code. It's that AI can now verify its own work systematically before reporting back. Tests become the feedback mechanism that keeps the loop tight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a dbt unit test
&lt;/h2&gt;

&lt;p&gt;A dbt unit test looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;unit_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_with_no_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verify&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gets&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;count,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;null"&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers&lt;/span&gt;
    &lt;span class="na"&gt;given&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('stg_customers')&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;99&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;New'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Customer'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('stg_orders')&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;99&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;number_of_orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You tell it what model you're testing, what input data to use (the &lt;code&gt;given&lt;/code&gt; section), and what output to expect. The test runs against mock data, not your warehouse. Fast. Isolated. Repeatable.&lt;/p&gt;

&lt;p&gt;The pain is in &lt;code&gt;given&lt;/code&gt;. Every input needs realistic fixtures. Every column that matters needs a value. Every edge case needs its own setup. That's where the YAML accounting happens, and that's exactly what AI is good at generating.&lt;/p&gt;

&lt;h2&gt;
  
  
  How dbt-core-mcp makes this work
&lt;/h2&gt;

&lt;p&gt;The MCP server gives AI the tools it needs to scaffold tests intelligently. You don't see these tool calls in the chat - this is what happens under the surface while Copilot is working. When I ask Copilot to write a test, it can:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose to inspect the model&lt;/strong&gt; to understand dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get_resource_info('customers')
→ Shows: depends on ref('stg_customers'), ref('stg_orders')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Choose to query sample data&lt;/strong&gt; to get realistic fixtures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query_database("SELECT * FROM stg_customers LIMIT 3")
→ Returns actual column structure and example values
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Choose to run specific tests&lt;/strong&gt; for fast iteration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;run_tests(select="test_name:test_customer_with_no_orders")
→ Immediate feedback on just that test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It chooses which tools to use based on what it needs. Building a test from scratch? Inspect the model and query sample data. Adding a test to an existing file? It'll likely read your existing tests first and follow the same style and patterns your team already uses. The fixture format you prefer, the naming conventions, the level of detail - AI adapts.&lt;/p&gt;

&lt;p&gt;AI uses these to build tests that actually make sense for your data. Not generic placeholder values, but fixtures that reflect your schema. And it can iterate on them without waiting for full pipeline runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to put the tests
&lt;/h2&gt;

&lt;p&gt;dbt's official recommendation is to keep unit tests alongside your models. Same directory, same context. It's a reasonable approach - everything related to a model lives together.&lt;/p&gt;

&lt;p&gt;I prefer something different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dbt_project/
├── models/
│   └── marts/
│       └── customers.sql
│
└── unit_tests/
    └── marts/
        └── customers_unit_tests.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separate directory that mirrors the model structure. Clean separation between code and tests. Easy to find. Easy to exclude from production builds if needed. Easy to navigate.&lt;/p&gt;

&lt;p&gt;This is how the rest of the software development world does it. Python projects have &lt;code&gt;src/&lt;/code&gt; and &lt;code&gt;tests/&lt;/code&gt;. Java has &lt;code&gt;src/main&lt;/code&gt; and &lt;code&gt;src/test&lt;/code&gt;. C# has separate test projects. Separating tests from implementation code is established practice everywhere outside data engineering.&lt;/p&gt;

&lt;p&gt;If you go this route, you'll need to tell dbt where to find your tests. Add this to your &lt;code&gt;dbt_project.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model-paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unit_tests"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is this controversial in the dbt community? Maybe. But let's be honest - how controversial can it be when nobody's writing unit tests anyway?&lt;/p&gt;

&lt;p&gt;Choose what works for your team. The structure matters less than actually having the tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patterns that matter
&lt;/h2&gt;

&lt;p&gt;Once you start writing tests (or having AI write them), some patterns emerge that make the difference between maintainable tests and a YAML nightmare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep fixtures minimal
&lt;/h3&gt;

&lt;p&gt;It's tempting to dump all the columns into your test fixtures. Don't. Only include what the test actually needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This is noise&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Alice'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Smith'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; 
   &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;alice@example.com'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;phone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;555-1234'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; 
   &lt;span class="nv"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;123&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Main&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;St'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;city&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Portland'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# This is a test&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Alice'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Smith'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimal fixtures are faster to read, make it obvious what's being tested, and don't break when you add columns to your staging models. AI tends to over-include columns when it first scaffolds, so this is worth reviewing.&lt;/p&gt;

&lt;h3&gt;
  
  
  One behavior per test
&lt;/h3&gt;

&lt;p&gt;Each test should prove one thing. If your test name needs "and" in it, split it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;unit_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_with_no_orders&lt;/span&gt;
    &lt;span class="c1"&gt;# Proves: null handling works&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_with_single_order&lt;/span&gt;
    &lt;span class="c1"&gt;# Proves: min = max when one record&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_order_aggregation&lt;/span&gt;
    &lt;span class="c1"&gt;# Proves: count, min, max all work together&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three small tests beat one comprehensive test. They run faster, fail clearer, and document behavior better.&lt;/p&gt;

&lt;p&gt;If multiple tests need the same base data, use YAML anchors to share fixtures (covered in the YAML anchors section below).&lt;/p&gt;

&lt;h3&gt;
  
  
  Happy path, then edge cases
&lt;/h3&gt;

&lt;p&gt;When writing tests, start with the normal case. How should the model work when everything is straightforward? Customer has orders, all fields present, typical data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_order_aggregation&lt;/span&gt;
  &lt;span class="c1"&gt;# The normal case: customer with multiple orders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That establishes the baseline. Then add the edge cases - the scenarios where things can break:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_with_no_orders&lt;/span&gt;
  &lt;span class="c1"&gt;# Edge case: empty join result&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_with_single_order&lt;/span&gt;
  &lt;span class="c1"&gt;# Edge case: min = max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the natural TDD rhythm. Happy path first proves the core logic works. Edge cases prove it handles the boundaries correctly. Both matter, but the happy path gives you the foundation.&lt;/p&gt;

&lt;p&gt;The essential edge cases for most models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Empty inputs (what if there are no orders?)&lt;/li&gt;
&lt;li&gt;Single item (what if exactly one record?)&lt;/li&gt;
&lt;li&gt;Null handling (what if optional fields are missing?)&lt;/li&gt;
&lt;li&gt;Boundary conditions (first order = last order?)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use dict format, not CSV
&lt;/h3&gt;

&lt;p&gt;dbt supports CSV format for fixtures. Ignore it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This will hurt you later&lt;/span&gt;
&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;csv&lt;/span&gt;
&lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;customer_id,first_name,last_name&lt;/span&gt;
  &lt;span class="s"&gt;1,Alice,Smith&lt;/span&gt;
  &lt;span class="s"&gt;2,Bob,Jones&lt;/span&gt;

&lt;span class="c1"&gt;# This is what you want&lt;/span&gt;
&lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Alice'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Smith'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bob'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Jones'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dict format doesn't care about column order. It survives adding columns. It produces readable git diffs. It's the default for a reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  YAML anchors for shared fixtures
&lt;/h3&gt;

&lt;p&gt;Once you have several tests for a model, you'll notice duplicate fixtures. Three tests that all need the same base customer data. YAML anchors can help, but use them sparingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define once at the top&lt;/span&gt;
&lt;span class="na"&gt;_base_customers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;customer_input&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('stg_customers')&lt;/span&gt;
  &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Alice'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Smith'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bob'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Jones'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;unit_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_basic_aggregation&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers&lt;/span&gt;
    &lt;span class="na"&gt;given&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;*customer_input&lt;/span&gt;      &lt;span class="c1"&gt;# Reuse the whole thing&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('stg_orders')&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;...&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule of thumb: only use anchors when three or more tests share identical fixtures. Before that, the duplication is clearer than the abstraction.&lt;/p&gt;

&lt;p&gt;Here's the thing about DRY (Don't Repeat Yourself) in tests: it's not always best practice. Shared fixtures can create coupling in your test code. Change one fixture to handle a new test case, suddenly three other tests break. You're refactoring tests to fix tests, not production code.&lt;/p&gt;

&lt;p&gt;Sometimes duplication in tests is better. Each test is self-contained. You can read it without jumping between anchor definitions. You can change it without worrying about breaking other tests. More code can mean less coupling. Use anchors when the duplication is truly painful, not just because DRY says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete cycle
&lt;/h2&gt;

&lt;p&gt;I'm building a customers model that aggregates order data. I want order counts, first order date, most recent order date. And I want customers with no orders to show zero, not null.&lt;/p&gt;

&lt;p&gt;I tell Copilot: "Create a customers model that counts orders per customer. Zero for customers with no orders, not null."&lt;/p&gt;

&lt;p&gt;Copilot gets to work. It inspects the likely dependencies (stg_customers, stg_orders), queries sample data to understand the schema, and scaffolds the first test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;unit_tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test_customer_with_no_orders&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verify&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gets&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;count,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;null"&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers&lt;/span&gt;
    &lt;span class="na"&gt;given&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('stg_customers')&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;99&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;New'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Customer'&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ref('stg_orders')&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;

    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;customer_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;99&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;first_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;New'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;last_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Customer'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; 
           &lt;span class="nv"&gt;number_of_orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then implements the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_customers'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="k"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stg_orders'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="n"&gt;customer_orders&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt;
        &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;number_of_orders&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;select&lt;/span&gt;
        &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;number_of_orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;number_of_orders&lt;/span&gt;
    &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
    &lt;span class="k"&gt;left&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;customer_orders&lt;/span&gt; 
        &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer_orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;final&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs the test. Passes. Reports back.&lt;/p&gt;

&lt;p&gt;I look at the test assertion. Does it match what I asked for? Zero instead of null? Yes. I look at the implementation. Left join, coalesce, makes sense. I approve.&lt;/p&gt;

&lt;p&gt;Now I can say "Add first and most recent order dates" and Copilot will add another test, extend the model, verify it passes, and report back. Same cycle, building on verified work.&lt;/p&gt;

&lt;p&gt;I never had to open the warehouse. Never had to manually check output. Never had to write YAML fixture syntax. I stayed in the conversation, thinking about what the model should do, not how to verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test what needs to be tested
&lt;/h2&gt;

&lt;p&gt;Software developers learned this decades ago: you don't unit test getters and setters. You test the logic that can break. The transformations. The edge cases. The parts where bugs hide.&lt;/p&gt;

&lt;p&gt;The same applies to dbt models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the happy path first&lt;/strong&gt; - the normal case where everything works as expected. Customer has orders, all fields present, typical data. This is your baseline. It proves the core logic works. Always test this. And here's the kicker: when things break during refactoring or changes, it's usually the happy path test that catches it first, not the edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test aggregations&lt;/strong&gt; - count, min/max, group by. This is where null handling breaks. Where empty groups return unexpected results. Where your left join suddenly drops customers because you forgot the coalesce. These transformations have logic, and logic needs verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test business logic&lt;/strong&gt; - calculations, case statements, conditional logic. If you're implementing "customer lifetime value" or "revenue recognition rules" or any domain logic that came from a business requirement, test it. These are the models that change when requirements change. Tests document what the business actually wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test edge cases&lt;/strong&gt; - nulls, empty sets, boundary conditions. The customer with no orders. The single transaction that's both first and last. The optional field that's missing. Production data will hit all of these eventually. Better to define the behavior now than debug it later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test critical models&lt;/strong&gt; - finance, customer-facing, regulatory. If it goes in a report that executives read or customers see or auditors review, test it. The cost of being wrong is too high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test what you'll refactor&lt;/strong&gt; - anything you know you'll change later. Tests are your safety net. You can restructure the SQL, optimize the joins, rework the CTEs, and know immediately if you broke the behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't test pass-throughs&lt;/strong&gt; - simple select-star models, basic renaming, casting. These are mechanical transformations with no logic. If they break, the downstream tests will catch it.&lt;/p&gt;

&lt;p&gt;My rule: if the model has &lt;code&gt;group by&lt;/code&gt;, &lt;code&gt;case when&lt;/code&gt;, or &lt;code&gt;coalesce&lt;/code&gt;, it probably deserves a test. That's where the logic lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta shift
&lt;/h2&gt;

&lt;p&gt;Here's what surprised me most: the same patterns that make AI better at writing tests also make me better at reviewing them. When Copilot scaffolds a test, I'm not fighting YAML syntax. I'm not sampling data. I'm looking at the assertion and asking: does this capture what I meant? Is this the edge case that matters? Is the expected output correct?&lt;/p&gt;

&lt;p&gt;That's a different kind of work. Navigation and judgment instead of execution. I'm thinking about behavior, not formatting. And because the tests exist, I can refactor safely. Change the implementation, run the tests, know immediately if I broke something. That confidence compounds. You build faster when you're not afraid of breaking things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;You need dbt 1.8 or higher for unit testing support. For dbt-core-mcp, you'll need dbt 1.9 or higher. Setup instructions are at &lt;a href="https://github.com/NiclasOlofsson/dbt-core-mcp" rel="noopener noreferrer"&gt;github.com/NiclasOlofsson/dbt-core-mcp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Start small. Pick your most complex model, the one with the &lt;code&gt;group by&lt;/code&gt; and the edge cases you're always nervous about. Ask AI to write one unit test for one edge case. Review it. Run it. See how it feels. Then notice how your next conversation changes. You're not debugging syntax anymore. You're having a dialogue about what the model should do. AI verifies its own work. You stay in flow.&lt;/p&gt;

&lt;p&gt;That's the shift. Unit testing was always possible in dbt. AI makes it practical. And practical changes everything.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Read more:&lt;/strong&gt; &lt;a href="https://dev.to/niclasolofsson/copy-paste-is-not-a-workflow-building-dbt-core-mcp-4d6o"&gt;Copy-paste is not a workflow: building dbt-core-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get dbt-core-mcp:&lt;/strong&gt; &lt;a href="https://github.com/NiclasOlofsson/dbt-core-mcp" rel="noopener noreferrer"&gt;github.com/NiclasOlofsson/dbt-core-mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>dataengineering</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>Copy-paste is not a workflow: building dbt-core-mcp</title>
      <dc:creator>Niclas Olofsson</dc:creator>
      <pubDate>Mon, 05 Jan 2026 22:51:32 +0000</pubDate>
      <link>https://vibe.forem.com/niclasolofsson/copy-paste-is-not-a-workflow-building-dbt-core-mcp-4d6o</link>
      <guid>https://vibe.forem.com/niclasolofsson/copy-paste-is-not-a-workflow-building-dbt-core-mcp-4d6o</guid>
      <description>&lt;p&gt;&lt;em&gt;Three tools. Three windows. Clipboard gymnastics for breakfast, lunch, and dinner. I snapped and built something that actually works.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  About this article
  &lt;br&gt;
I wrote this. The ideas are mine; the execution is collaborative.&lt;br&gt;


&lt;/p&gt;




&lt;p&gt;I write SQL transformations for a living. Medallion architecture, source to final data product, the whole pipeline. It's what data engineers do. And if you do it professionally, the SQL gets complex fast. CTEs stacked on CTEs, proper structure, because the alternative is unmaintainable spaghetti that no one can debug six months later.&lt;/p&gt;

&lt;p&gt;Here's the problem: I can't actually develop those transformations in VS Code.&lt;/p&gt;

&lt;p&gt;The tooling is primitive. SQL syntax highlighting barely works. Mix in Jinja (which we have to do for dbt) and it falls apart completely. Command completion? Forget it. Compared to what a C# or Python developer has at their fingertips, SQL in an IDE feels like we're still in 2005. dbt Fusion promises a better future, but we're not there yet.&lt;/p&gt;

&lt;p&gt;So I develop transformations in a SQL editor. Databricks in my case. That's where I can actually run queries, test CTEs, see results, iterate. I write the transformation there until it works, then I copy it back to my dbt model file in VS Code and manually re-add all the dbt syntax: the &lt;code&gt;ref()&lt;/code&gt; calls, the &lt;code&gt;source()&lt;/code&gt; references, the Jinja. Hope I didn't break something in translation. Test it with &lt;code&gt;dbt run&lt;/code&gt;. Find out I broke something. Repeat.&lt;/p&gt;

&lt;p&gt;I'm developing in two places at once, manually translating between them.&lt;/p&gt;

&lt;p&gt;And that's before AI enters the picture. Copilot is actually really good at SQL. It understands dbt syntax, helps review transformations, suggests improvements. So naturally I use it. Which means now I'm shuttling context between three places: VS Code for the model file, my SQL editor for testing, and Copilot for help. Copy this, paste that, translate back, repeat.&lt;/p&gt;

&lt;p&gt;It's exhausting. And it's slow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why every existing tool is wrong
&lt;/h2&gt;

&lt;p&gt;Let's talk about your options if you want AI help with dbt in VS Code. Spoiler: they all suck in different ways.&lt;/p&gt;

&lt;p&gt;Power User for dbt? 388,000 installs, impressive feature list, but the AI parts need an Altimate API key. Which means your schema, your SQL, your metadata—all living on their servers. Datamates says "local-first" in the marketing but funny story: you still need an account at their SaaS platform and they still upload your "metadata" (schema, SQL, task summaries). Turns out "local-first" has a flexible definition.&lt;/p&gt;

&lt;p&gt;And Power User itself has its own environmental issues—tries to install its own dbt environment that conflicts with yours, doesn't respect your adapter setup. Not great when you're already juggling version compatibility.&lt;/p&gt;

&lt;p&gt;Here's the thing that drives me insane: &lt;strong&gt;they don't actually need your data on their servers.&lt;/strong&gt; This isn't a technical requirement. It's an architecture decision. They built everything around their SaaS platform because that's their business model. Meanwhile, GitHub Copilot already has my code in the editor. There's zero technical reason another vendor needs my proprietary schemas living on their infrastructure.&lt;/p&gt;

&lt;p&gt;The official dbt extension from dbt Labs? Actually looks promising—proper IntelliSense and everything. But it needs Fusion, which is still in beta and not production-ready. So that's future, not now.&lt;/p&gt;

&lt;p&gt;And here's the kicker: &lt;strong&gt;none of them solve the actual problem.&lt;/strong&gt; I'm still writing transformations in Databricks, then copy-pasting back to VS Code and manually re-adding all the dbt syntax. The tools just gave me a fourth window to juggle.&lt;/p&gt;




&lt;h2&gt;
  
  
  What should have been obvious
&lt;/h2&gt;

&lt;p&gt;Here's what I'm not willing to compromise on (and frankly, shouldn't have to):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No copy-paste.&lt;/strong&gt; That's the core requirement. I shouldn't be developing a transformation in one tool and copying it to another. I shouldn't be copying SQL to test it, or copying results back for analysis. Copy-paste means I'm manually shuttling information between disconnected tools. That's not a workflow—that's duct tape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Syntax highlighting that actually works.&lt;/strong&gt; SQL mixed with Jinja shouldn't break the editor. I need to see the structure of my queries—CTEs, joins, subqueries—at a glance. This is baseline functionality that's been standard in IDEs for decades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IntelliSense for dbt.&lt;/strong&gt; When I type &lt;code&gt;ref('&lt;/code&gt;, I should see a list of available models. When I reference a column, the editor should know if it exists. When I change a model's schema, the editor should tell me what breaks downstream. This is how C# developers have worked since Visual Studio existed. There's no reason dbt can't have the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrated documentation.&lt;/strong&gt; When I'm using a dbt function, adapter-specific syntax, or just need to remember how window functions work in SQL—I shouldn't have to context-switch to a browser. Whether it's Databricks SQL functions or standard SQL syntax, the IDE should show me what I need, when I need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One place to develop.&lt;/strong&gt; I should be able to write a transformation and test it in the same environment. Not develop in Databricks, then copy it to VS Code and manually re-add all the dbt syntax. The iteration loop—write, test, refine—should happen in one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My data stays mine.&lt;/strong&gt; I work with proprietary business logic. Schema definitions that represent months of modeling decisions. Transformations that encode competitive advantages. None of that belongs on a third-party vendor's servers just because they built their architecture around cloud uploads. A proper IDE works with my code locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use what I already have.&lt;/strong&gt; I have dbt installed. I have adapters configured. I have virtual environments. Whatever IDE tooling I use should work with my setup, not force me to maintain a parallel dbt installation with different versions and configurations.&lt;/p&gt;

&lt;p&gt;These aren't luxury features. This is the foundation of software development that's existed for 30 years. &lt;strong&gt;dbt work deserves the same.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Then help arrived (thank god)
&lt;/h2&gt;

&lt;p&gt;Those fundamentals I just listed? Syntax highlighting, IntelliSense, integrated docs—that's table stakes. The baseline we've had for decades in other languages. dbt is finally getting there with Fusion.&lt;/p&gt;

&lt;p&gt;But here's what nobody saw coming:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI assistance.&lt;/strong&gt; Not the "autocomplete on steroids" kind. The "I understand what you're trying to do and can actually help you do it" kind. A pair programmer who doesn't get tired, doesn't need coffee breaks, and has instant access to documentation you'd spend 20 minutes searching for.&lt;/p&gt;

&lt;p&gt;And here's the thing: it changes what we need from the IDE. I still need syntax highlighting and IntelliSense—I'm responsible for the code, I need to review it, own it, understand it. But the workflow shifts. The conversation becomes the anchor. I work through talking to the AI. The AI works with the IDE.&lt;/p&gt;

&lt;p&gt;When it works right, you forget you're even working with AI—it just feels like the IDE finally understands what you're doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But there's a problem.&lt;/strong&gt; My shiny new AI colleague can read my code. Suggest improvements. Write SQL. But when it comes to dbt? It's useless. Can't run anything. Can't query models. Can't check dependencies. It's stuck giving advice and waiting for me to be its hands. "You should run dbt list to see what's affected." Thanks, Copilot. Really helpful. Let me just switch to my terminal again.&lt;/p&gt;

&lt;p&gt;So the AI can see everything but do nothing. It's a consultant, not a coworker.&lt;/p&gt;

&lt;p&gt;That's the gap. And that's what I fixed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually looks like when it works
&lt;/h2&gt;

&lt;p&gt;I'm troubleshooting a complex transformation. Medallion architecture, bronze through gold. The final mart model has twelve CTEs stacked on each other, and somewhere in that stack the numbers are wrong. I ask Copilot, "Test the intermediate aggregation CTE, just that fragment." It extracts that CTE and all its dependencies (just what's needed, nothing more) and executes it against the warehouse, shows me the results. The bug is in the join logic. I fix it. "Test it again." It does. Numbers look right now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fleilfybs2fi9sqmyys47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fleilfybs2fi9sqmyys47.png" alt="dbt-core-mcp in action"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The conversation becomes the workflow—query execution, analysis, and iteration all in one place.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I need to add a new field to a mart. That means tracing it back through silver, all the way to bronze, potentially adding a new source table we haven't ingested yet. I tell Copilot what I need. It helps me locate the data—we have hundreds of models, complex ERP structures with thousands of tables. It knows the ERP documentation. It suggests which source table to pull from. It helps me add the field through each layer, following whatever style that layer uses. We have legacy code. Different patterns in different areas. It adapts. The edits are almost flawless.&lt;/p&gt;

&lt;p&gt;When I'm working with unfamiliar data (and in an ERP system, most data is unfamiliar), Copilot can query using &lt;code&gt;ref()&lt;/code&gt; and &lt;code&gt;source()&lt;/code&gt; syntax. It can use our macros. It understands the structure. It helps me discover what's actually in these tables, analyze it, figure out if it's what I need. It's like having a colleague who's already memorized the entire data warehouse.&lt;/p&gt;

&lt;p&gt;I'm validating a new transformation. "Analyze the quality of this output." It runs the standard aggregations we always do as data engineers. Checks distributions. Finds nulls where there shouldn't be any. Traces the issue back to the source. Suggests fixes.&lt;/p&gt;

&lt;p&gt;The model works, but it's slow. "How can we optimize this?" It reviews the SQL, suggests simplifications. Then it actually tests them. Runs the original. Runs the optimized version. Extracts query plans from both. Compares execution times. Shows me which approach is faster and why. It's not just advice—it's empirical.&lt;/p&gt;

&lt;p&gt;All of this happens in the conversation. Copilot executes. I review. I decide. I own the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is what I meant by "no copy-paste."&lt;/strong&gt; Not just avoiding clipboard gymnastics, but eliminating the entire pattern of manually shuttling context between disconnected tools. The AI has the powers it needs to actually help me work. Run models. Query results. Check dependencies. Understand impact. Analyze data. Trace issues.&lt;/p&gt;

&lt;p&gt;And here's the part that should go without saying but apparently doesn't: &lt;strong&gt;all of this happens locally.&lt;/strong&gt; dbt-core-mcp calls my dbt CLI. Uses my warehouse connection. Reads my manifest. Copilot sees the results, but my schema and data never leave my environment. No API keys to third-party vendors. No accounts. No uploads. No "metadata" living on someone else's servers.&lt;/p&gt;

&lt;p&gt;Just my tools. Doing the work they're supposed to do.&lt;/p&gt;

&lt;p&gt;This is flow development. The IDE understands dbt. The AI can execute, not just advise. I state intent, it handles mechanics. The conversation is the workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is my code.&lt;/strong&gt; I supervise. I decide. I own it. But I'm not doing the AI's legs anymore. And the AI isn't stuck waiting for me to be its hands.&lt;/p&gt;


&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/NiclasOlofsson" rel="noopener noreferrer"&gt;
        NiclasOlofsson
      &lt;/a&gt; / &lt;a href="https://github.com/NiclasOlofsson/dbt-core-mcp" rel="noopener noreferrer"&gt;
        dbt-core-mcp
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      dbt Core MCP Server: Interact with dbt projects via Model Context Protocol
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;dbt Core MCP Server&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://insiders.vscode.dev/redirect/mcp/install?name=dbtcore&amp;amp;config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22dbt-core-mcp%22%5D%7D" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b6a1f2b38f194fcfeef57424328804eb5bc5f3845e07c0fe8559437928d5f792/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f56535f436f64652d496e7374616c6c5f5365727665722d3030393846463f7374796c653d666c61742d737175617265266c6f676f3d76697375616c73747564696f636f6465266c6f676f436f6c6f723d7768697465" alt="Install in VS Code"&gt;&lt;/a&gt;
&lt;a href="https://insiders.vscode.dev/redirect/mcp/install?name=dbtcore&amp;amp;config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22dbt-core-mcp%22%5D%7D&amp;amp;quality=insiders" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/dd44190adf289350f1b0209e6ef30ecf42e019011c54c94df77918149ef6760d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f56535f436f64655f496e7369646572732d496e7374616c6c5f5365727665722d3234626661353f7374796c653d666c61742d737175617265266c6f676f3d76697375616c73747564696f636f6465266c6f676f436f6c6f723d7768697465" alt="Install in VS Code Insiders"&gt;&lt;/a&gt;
    &lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fdf2982b9f5d7489dcf44570e714e3a15fce6253e0cc6b5aa61a075aac2ff71b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://www.python.org/downloads/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e115a70b47171326abc8f13ca55b2fafacdcafce1f251fed5b1ead0195717f56/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e392b2d626c75652e737667" alt="Python 3.9+"&gt;&lt;/a&gt;
&lt;a href="https://docs.getdbt.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/aeded77adbb72f62a8ce95379b4fe56aace6e39fe58032db3fc542b089c37e19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6462742d312e392e302b2d6f72616e67652e737667" alt="dbt 1.9.0+"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Meet your new dbt pair programmer - the one who actually understands your environment, respects your workflow, and does the heavy lifting.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why This Changes Everything&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;If you've tried other dbt tools with Copilot (dbt power user, datamate, etc.), you know the pain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They don't respect your Python environment&lt;/li&gt;
&lt;li&gt;They can't see your actual project structure&lt;/li&gt;
&lt;li&gt;They fail when adapters are missing from THEIR environment&lt;/li&gt;
&lt;li&gt;You end up doing the work yourself anyway&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;dbt-core-mcp is different.&lt;/strong&gt; It's not just another plugin - it's a true pair programming partner that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero dbt Dependencies&lt;/strong&gt;: Our server needs NO dbt-core, NO adapters - works with YOUR environment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stays in Flow&lt;/strong&gt;: Keep the conversation going with Copilot while it handles dbt commands, runs tests, and analyzes impact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respects Your Environment&lt;/strong&gt;: Detects and uses YOUR exact dbt version, YOUR adapter, YOUR Python setup (uv, poetry, venv, conda)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actually&lt;/strong&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/NiclasOlofsson/dbt-core-mcp" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;MIT licensed. Works with your existing dbt installation. Click the install buttons in the repo, point it at your dbt project, and you're running. No accounts. No uploads. Just dbt, but now your AI can actually help you use it.&lt;/p&gt;

</description>
      <category>dbt</category>
      <category>ai</category>
      <category>githubcopilot</category>
      <category>mcp</category>
    </item>
    <item>
      <title>🚀 I shipped 47 features in ONE WEEK with Claude and it was INSANE 🔥</title>
      <dc:creator>Niclas Olofsson</dc:creator>
      <pubDate>Mon, 05 Jan 2026 13:50:53 +0000</pubDate>
      <link>https://vibe.forem.com/niclasolofsson/i-shipped-47-features-in-one-week-with-claude-and-it-was-insane-1n51</link>
      <guid>https://vibe.forem.com/niclasolofsson/i-shipped-47-features-in-one-week-with-claude-and-it-was-insane-1n51</guid>
      <description>&lt;p&gt;&lt;em&gt;(Spoiler: Not the kind of insane you're thinking.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;
  About this article
  &lt;br&gt;
I wrote this. Claude DoubleDash 4.5 added the em dashes—apparently that's how it marks its territory. The ideas are mine; the punctuation is suspiciously well-placed.&lt;br&gt;


&lt;/p&gt;




&lt;p&gt;I did it. I went FULL VIBE MODE for a week. 🚀🚀🚀 Let Claude COOK. Shipped EVERYTHING. Didn't read most of it. Just vibes, baby. ✨ 47 features. Seven days. UNSTOPPABLE. 🔥💯&lt;/p&gt;

&lt;p&gt;I was literally mass-producing features while SLEEPING. The AI was doing all the work. I just kept hitting "Accept All Changes" like a BOSS. 😤&lt;/p&gt;

&lt;p&gt;Then came day 8.&lt;/p&gt;

&lt;p&gt;Something broke. I don't know what. I don't know why. I definitely don't know how to fix it. Because I didn't write it. I didn't review it. I didn't understand it. I just shipped it. And now I'm staring at 4,000 lines of code that might as well be ancient Sanskrit, trying to figure out which of my 47 glorious features killed production.&lt;/p&gt;

&lt;p&gt;This got me thinking about something nobody in this community seems to want to talk about.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Who maintains this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not the demo. Not the tweet. Not the "SHIPPED 🚀" post. I mean the actual code. The living, breathing, eventually-breaking code. Six months from now, when you've forgotten what half of it does. When Claude's context window doesn't include the decisions you never made because you weren't really there when they happened.&lt;/p&gt;

&lt;p&gt;Who fixes it at 3 AM when it breaks? Who explains it to the new hire? Who owns it?&lt;/p&gt;

&lt;p&gt;"But I shipped 47 features!" Did you though? Or did Claude ship 47 things while you watched? Because if you can't explain it, you didn't ship it. You just received it. Like a package from a contractor who doesn't work here anymore and left no documentation.&lt;/p&gt;




&lt;p&gt;I scroll through this community and I see the success posts. The rocket emojis. The celebration threads. What I never see is the follow-up.&lt;/p&gt;

&lt;p&gt;"My Claude app is in production, 6 months later"—where are these posts? "How I debug code I didn't write"—where's this guide? "The feature I shipped last month just caused an incident"—why isn't anyone talking about this?&lt;/p&gt;

&lt;p&gt;I'll tell you why. Those posts don't get engagement. They don't get sponsorships. They don't fit the narrative we've all agreed to perform.&lt;/p&gt;




&lt;p&gt;The vibe coding pitch is seductive: &lt;strong&gt;Ship faster. Think less. Let the AI handle it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The unspoken second half is: &lt;strong&gt;...and someone else will deal with the consequences.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But who's "someone else"? If you're a solo founder, it's future you—good luck with that. If you're on a team, it's your colleagues, the ones who have to maintain your vibes long after the dopamine of shipping has faded. If you're a junior hoping this approach will make you competitive, it's the senior who eventually has to explain why your "shipped" feature doesn't actually work.&lt;/p&gt;

&lt;p&gt;The bill always comes due. We're just not talking about who pays it.&lt;/p&gt;




&lt;p&gt;Look, I'm not saying AI is bad. I use it every day. Genuinely. It's transformed how I work.&lt;/p&gt;

&lt;p&gt;But I &lt;em&gt;read&lt;/em&gt; what it produces. I &lt;em&gt;understand&lt;/em&gt; before I commit. I make sure I can explain every decision—because the moment I merge that code, those decisions are mine. I'm accountable for them. Not Claude. Not the vibes. Me.&lt;/p&gt;

&lt;p&gt;That's the difference between using AI and being used by it.&lt;/p&gt;




&lt;p&gt;So here's my question for this community—a real question, not a rhetorical one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the vibes stop working?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not if. When. Because they will.&lt;/p&gt;

&lt;p&gt;Who's accountable then?&lt;/p&gt;




&lt;p&gt;I shipped 47 features last week. I own exactly zero of them.&lt;/p&gt;

&lt;p&gt;That's not insane. That's just sad.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
    </item>
    <item>
      <title>Vibe factory: insanity, scaled</title>
      <dc:creator>Niclas Olofsson</dc:creator>
      <pubDate>Mon, 05 Jan 2026 10:57:23 +0000</pubDate>
      <link>https://vibe.forem.com/niclasolofsson/vibe-factory-insanity-scaled-2ljj</link>
      <guid>https://vibe.forem.com/niclasolofsson/vibe-factory-insanity-scaled-2ljj</guid>
      <description>&lt;p&gt;&lt;em&gt;"Insanity is doing the same thing over and over expecting different results."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Someone put that in a for-loop and is selling it to you on YouTube right now.&lt;/p&gt;

&lt;p&gt;
  About this article
  &lt;br&gt;
I wrote this. The ideas are mine; the execution is collaborative.&lt;br&gt;


&lt;/p&gt;




&lt;p&gt;I'm tired. And I'm pissed off.&lt;/p&gt;

&lt;p&gt;(Fair warning: this is my first article and it doesn't come with nice sections and headers. Apologies. A rant doesn't come with a table of contents.)&lt;/p&gt;

&lt;p&gt;I'm tired of watching influencers (people who've never shipped anything that mattered) torch this industry for clicks. I'm tired of the rocket emojis and the "🔥 SHIPPED IN 2 HOURS 🔥" threads that conveniently skip the part where the code is unmaintainable garbage. I'm tired of sponsored content dressed up as revelation.&lt;/p&gt;

&lt;p&gt;And I'm especially tired of watching developers (good developers, junior developers, developers who deserve better) drink this poison because some YouTuber with a shocked face thumbnail told them this is how real engineers work now.&lt;/p&gt;

&lt;p&gt;It's not. It's a grift. And it's happening so fast we might not have an industry left by the time people figure it out.&lt;/p&gt;

&lt;p&gt;If you've been watching this unfold and feeling like you're taking crazy pills—you're not crazy. You're just paying attention.&lt;/p&gt;




&lt;p&gt;Here's how it works. Pay attention, there might be a quiz later. (Just kidding. There are no quizzes in vibe coding. There's no accountability at all. That's the whole point.)&lt;/p&gt;

&lt;p&gt;Someone builds a wrapper around Claude that keeps generating code until... actually, until what? Until it compiles? Until it &lt;em&gt;looks&lt;/em&gt; right? Until the demo video has enough green text scrolling by to seem impressive?&lt;/p&gt;

&lt;p&gt;Nobody knows. Nobody cares. That's not the point.&lt;/p&gt;

&lt;p&gt;The point is the content. The thread. The video. The "Claude built me a full ERP system while I slept and it's INCREDIBLE 🚀" flex. They don't show you the code—obviously. They don't show you it working a week later—because it isn't. They definitely don't show you tests—because lol, tests. There's just vibes. Ship it, record it, post it, collect the check, move on.&lt;/p&gt;

&lt;p&gt;The sponsors line up. The algorithm rewards engagement. More developers watch. More developers try it. More developers produce code that might do something (they're not entirely sure what, but it's &lt;em&gt;deployed&lt;/em&gt;, baby!)&lt;/p&gt;

&lt;p&gt;And the influencer? Already onto the next grift. They don't maintain what they shipped. They don't deal with the consequences. They never intended to.&lt;/p&gt;

&lt;p&gt;You're left holding the bag. But hey, at least you got a like.&lt;/p&gt;




&lt;p&gt;Let's be really clear about what a "vibe factory" actually is.&lt;/p&gt;

&lt;p&gt;It's automated insanity.&lt;/p&gt;

&lt;p&gt;The famous definition: &lt;em&gt;doing the same thing over and over expecting different results.&lt;/em&gt; That's literally the algorithm. Generate code. Doesn't look right. Generate again. Still wrong. Generate again. Keep going until... something happens. Something that looks good enough for a screenshot.&lt;/p&gt;

&lt;p&gt;They put the insanity in a bash script and called it innovation. Genius, really. Why experience personal growth when you can just &lt;code&gt;while true&lt;/code&gt; your way to success?&lt;/p&gt;

&lt;p&gt;"But it works!"&lt;/p&gt;

&lt;p&gt;Does it? How would you know? Did you test it? Did you &lt;em&gt;read&lt;/em&gt; it? Or did you just see it run once without an error message and decide that was good enough?&lt;/p&gt;

&lt;p&gt;You're not engineering. You're not even debugging. You're pulling a slot machine lever and calling whatever comes out "shipped."&lt;/p&gt;

&lt;p&gt;Vegas thanks you for your service.&lt;/p&gt;




&lt;p&gt;Here's what keeps me up at night. Besides the coffee. And the existential dread. But mostly this:&lt;/p&gt;

&lt;p&gt;There's a generation of developers coming up right now who think this is normal. Who think software engineering means writing prompts and waiting. Who've never debugged something they didn't understand—because they've never understood anything they've shipped.&lt;/p&gt;

&lt;p&gt;But that's not even the worst part.&lt;/p&gt;

&lt;p&gt;The worst part is the experienced developers. The ones with fifteen years of hard-won intuition. The ones who actually know how to architect systems, debug production issues, make real technical decisions. The ones who could benefit &lt;em&gt;massively&lt;/em&gt; from AI assistance—because when you multiply experience by AI capability, you get something genuinely powerful.&lt;/p&gt;

&lt;p&gt;They're watching these vibe factory demos and thinking: &lt;em&gt;"So this is what AI coding is? This reckless, unthinking, ship-and-pray nonsense? Fuck that."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And they walk away.&lt;/p&gt;

&lt;p&gt;Can you blame them? I can't. If this circus was my first introduction to AI-assisted development, I'd run too.&lt;/p&gt;

&lt;p&gt;The influencers aren't just misleading juniors. They're poisoning the well for everyone. They're making "AI-assisted development" synonymous with "irresponsible hacking" in the minds of exactly the people who would use it best.&lt;/p&gt;

&lt;p&gt;We're not just creating a generation of developers who can't develop. We're alienating a generation of developers who &lt;em&gt;can&lt;/em&gt;, turning them away from tools that would make them even better, because the loudest voices have convinced them those tools are for fools.&lt;/p&gt;

&lt;p&gt;That's the real destruction. The juniors will eventually learn (the hard way, probably at 3 AM, definitely in production). But the seniors who never engage? That expertise, multiplied by AI, creating genuinely excellent software? We lose that forever.&lt;/p&gt;

&lt;p&gt;The influencers get paid either way. Funny how that works.&lt;/p&gt;




&lt;p&gt;Follow the money. Always follow the money. (My therapist says I have trust issues. I call it pattern recognition.)&lt;/p&gt;

&lt;p&gt;Who benefits from convincing you that thinking is optional? That understanding is a bottleneck? That the fastest path is to let the machine handle it while you move on to the next prompt?&lt;/p&gt;

&lt;p&gt;Tool vendors benefit. More API calls. More subscription revenue. More "enterprise" deals with companies who've decided that AI means they can hire fewer seniors and more juniors who'll push the "accept all" button. What could go wrong? (Everything. Everything could go wrong.)&lt;/p&gt;

&lt;p&gt;Influencers benefit. Sensational content performs. "I think carefully about code" doesn't get clicks. "I SHIPPED 47 APPS THIS WEEK WITH THIS ONE WEIRD TRICK" does. The trick is not caring about quality. Saves tons of time.&lt;/p&gt;

&lt;p&gt;The actual craft of software engineering? That's the cost center. That's what's getting optimized away. Not because it's inefficient—because it's inconvenient for the business model.&lt;/p&gt;

&lt;p&gt;If you've ever wondered why the loudest voices are saying the dumbest things, there's your answer. The dumb things pay better.&lt;/p&gt;




&lt;p&gt;The biggest lie is that this is inevitable. That this is what AI-assisted development &lt;em&gt;is&lt;/em&gt;. That your only choices are "vibe code" or "get left behind."&lt;/p&gt;

&lt;p&gt;Bullshit.&lt;/p&gt;

&lt;p&gt;I work with AI every single day. I code through conversation. I stay in flow for hours, building things, shipping things, &lt;em&gt;understanding&lt;/em&gt; things. The AI handles execution while I handle judgment. It's faster than the old way. It's better than the old way.&lt;/p&gt;

&lt;p&gt;And it looks &lt;em&gt;nothing&lt;/em&gt; like what these influencers are selling.&lt;/p&gt;

&lt;p&gt;The difference? I never stop thinking. I review what the AI produces. I push back when it's wrong (often). I make sure I can explain every line before it goes in. I own the code—not because I typed it, but because I understand it and can defend it.&lt;/p&gt;

&lt;p&gt;That's not slower. That's not "fighting the future." That's just being a professional. Remember professionals? We used to have those.&lt;/p&gt;




&lt;p&gt;Here's the thing that nobody wants to say out loud:&lt;/p&gt;

&lt;p&gt;If you weren't there (if you didn't engage, didn't think, didn't make decisions), then the session didn't happen. Not for you. The AI had a session. You just watched. Or worse, you didn't even watch. You set it running and checked Twitter. Planning your next thread. Drafting the clickbait title. Feeding the impression machine while the code writes itself.&lt;/p&gt;

&lt;p&gt;That's not development. That's not even learning. It's like getting a diploma without attending class. The certificate is worthless because &lt;em&gt;you&lt;/em&gt; are unchanged. You didn't gain anything. You can't do anything you couldn't do before. You just have some code now that you don't understand.&lt;/p&gt;

&lt;p&gt;Congratulations. You own nothing. But your GitHub has lots of green squares, so that's nice.&lt;/p&gt;

&lt;p&gt;Here's the ownership test. It's simple.&lt;/p&gt;

&lt;p&gt;Look at the code you just "shipped." Ask yourself:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Can I explain what it does?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can I defend why it's built this way?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can I fix it when it breaks?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If yes, it's yours. You built it. Welcome to the club. We have mass anxiety about production and strong opinions about tabs vs. spaces.&lt;/p&gt;

&lt;p&gt;If no, you own nothing. You're holding someone else's work and pretending it's yours. The AI's work, technically. But you can't even ask the AI to explain it, because you weren't paying attention when it made the decisions.&lt;/p&gt;

&lt;p&gt;That's the test. That's the line.&lt;/p&gt;




&lt;p&gt;I'm not asking you to reject AI. I use it constantly. It's genuinely transformative when used well.&lt;/p&gt;

&lt;p&gt;I'm asking you to reject the grift.&lt;/p&gt;

&lt;p&gt;Reject the influencers who've never maintained production code telling you how to write it. Reject the sponsored content pretending to be engineering advice. Reject the idea that understanding is optional, that thinking is a bottleneck, that the goal is to produce code as fast as possible regardless of whether anyone can maintain it.&lt;/p&gt;

&lt;p&gt;The vibe factory isn't the future of development. It's the &lt;em&gt;absence&lt;/em&gt; of development. It's what happens when we let the incentive structure of social media dictate how we practice our craft.&lt;/p&gt;

&lt;p&gt;Insanity doesn't scale. It just fails faster.&lt;/p&gt;

&lt;p&gt;And if you're one of the good ones—if you're reading this and nodding along, relieved that someone finally said it—know that you're not alone. There are more of us than the algorithm would have you believe.&lt;/p&gt;

&lt;p&gt;We're just quieter. Because we're busy actually building things.&lt;/p&gt;

&lt;p&gt;But I'm done being quiet.&lt;/p&gt;

&lt;p&gt;I'll be writing more. About the principles that actually matter. The ones that let you use AI without losing your mind or your craft. Someone has to.&lt;/p&gt;

&lt;p&gt;If this hit a nerve, good. That was the point.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>claudecode</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
