Oct 23, 20269 min read

Testing Backends Written by AI Coding Agents

How to test TypeScript and Go backends that a coding agent wrote, with integration tests against real Postgres and Pub/Sub instead of mocks, and a test command the agent can run on its own.

A backend written by a coding agent usually looks right, compiles and passes the tests the agent wrote next to it, which is exactly why it is risky, and in the 2025 Stack Overflow Developer Survey, 66 percent of developers said their biggest frustration with AI tools was solutions that are almost right but not quite, and 45 percent said debugging AI-generated code is more time-consuming.

Go developers describe the same pattern, since in the 2025 Go Developer Survey generating unit tests was one of the most cited benefits of AI tools, while respondents said the generated code still needed careful review and often corrections.

For a backend, almost right is a query that works on an empty table and fails on a unique constraint, a migration that the tests never ran, or a handler that publishes an event the subscriber cannot decode, and unit tests with a mocked database pass in every one of those cases.

What a test setup needs when an agent is the one running it:

  • One command that starts the database and queues the code uses, runs the tests and exits.
  • Real Postgres with the migrations applied, so SQL and schema mistakes fail the test.
  • A way to assert on side effects, such as published messages, not only on return values.
  • Isolation between tests, so an agent adding a test does not break an unrelated one.

Approaches to testing agent-written backends at a glance

ApproachDatabase in testsPub/Sub in testsSetup the agent maintains
Encore with encore testReal Postgres, migrated, separate from dev dataLocal topics, with published messages readable in Go testsNone beyond the test files
TestcontainersA Postgres container per suiteA broker container you configureContainer setup and connection wiring in each suite
Shared Docker Compose databaseOne long-running PostgresA broker in the Compose fileCompose file, migrations and cleanup between tests
MocksNoneNoneMock implementations that mirror the real interfaces
Compare the approaches in more detail
CapabilityEncore with encore testTestcontainersShared Docker Compose databaseMocks
Catches SQL and migration errorsYesYesYesif migrations run firstNo
Per-test isolationet.NewTestDatabase in Go, shared test database by defaultPer suite, per test is slowManual truncationYes
Test speed tuningTest databases skip fsync and use an in-memory filesystemDefault container settingsDefault settingsFast
Tracing of test requestsGo test traces in the local dashboardNot providedNot providedNot provided
Same declarations as productionYesprovisioned in your AWS or GCP on deployNoNoNo

Encore: encore test with real infrastructure

Encore

Encore combines a TypeScript and Go backend framework with infrastructure declared in the same code, so the test runner already knows which databases and topics the app uses. encore test sets that infrastructure up in test mode before running your tests, with separate test databases that skip fsync and use an in-memory filesystem, which keeps integration tests close to unit-test speed. The same command works for both languages, wrapping Vitest or another runner in TypeScript and go test in Go, and it supports the same flags.

In TypeScript, a test imports the endpoint and calls it like a function, which runs the real handler against the real test database.

// orders/orders.test.ts import { describe, expect, test } from "vitest"; import { create, get } from "./orders"; describe("orders", () => { test("stores the order in Postgres", async () => { const order = await create({ email: "ada@example.com", total: 40 }); const fetched = await get({ id: order.id }); expect(fetched.total).toBe(40); }); });

The service under test declares its database and topic at the top, and encore test finds and starts both. The get endpoint below sits next to the create endpoint, which inserts the order and publishes to orderPlaced.

// orders/orders.ts import { api, APIError } from "encore.dev/api"; import { Topic } from "encore.dev/pubsub"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const db = new SQLDatabase("orders", { migrations: "./migrations" }); export const orderPlaced = new Topic<{ orderId: number; email: string }>("order-placed", { deliveryGuarantee: "at-least-once", }); interface Order { id: number; email: string; total: number; } export const get = api( { expose: true, method: "GET", path: "/orders/:id" }, async ({ id }: { id: number }): Promise<Order> => { const row = await db.queryRow<Order>` SELECT id, email, total FROM orders WHERE id = ${id}`; if (!row) throw APIError.notFound("order not found"); return row; }, );
Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

The Go version of the service keeps the insert in a helper that takes the database as a parameter, so the same function runs against the service database in Create and against an isolated database in a test.

func insertOrder(ctx context.Context, db *sqldb.Database, email string, total int) (*Order, error) { o := &Order{Email: email, Total: total} err := db.QueryRow(ctx, `INSERT INTO orders (email, total) VALUES ($1, $2) RETURNING id`, email, total, ).Scan(&o.ID) return o, err }

Go tests use the standard testing package with Encore's et package for infrastructure. The first test below calls the endpoint, reads the order back and checks that exactly one event was published, the second confirms that validation rejects a zero total, and the third asks for a fresh, fully migrated database that only that test sees.

package orders import ( "context" "testing" "encore.dev/et" ) func TestCreate(t *testing.T) { ctx := context.Background() order, err := Create(ctx, &CreateParams{Email: "ada@example.com", Total: 40}) if err != nil { t.Fatal(err) } got, err := Get(ctx, order.ID) if err != nil { t.Fatal(err) } if got.Total != 40 { t.Fatalf("got total %d, want 40", got.Total) } msgs := et.Topic(OrderEvents).PublishedMessages() if len(msgs) != 1 || msgs[0].OrderID != order.ID { t.Fatalf("want one order-placed event for order %d, got %d", order.ID, len(msgs)) } } func TestCreateRejectsZeroTotal(t *testing.T) { _, err := Create(context.Background(), &CreateParams{Email: "ada@example.com", Total: 0}) if err == nil { t.Fatal("expected a validation error") } } func TestInsertOrderIsolated(t *testing.T) { ctx := context.Background() // A fresh, fully migrated database that only this test sees. testDB, err := et.NewTestDatabase(ctx, "orders") if err != nil { t.Fatal(err) } if _, err := insertOrder(ctx, testDB, "ada@example.com", 40); err != nil { t.Fatal(err) } var count int if err := testDB.QueryRow(ctx, `SELECT count(*) FROM orders`).Scan(&count); err != nil { t.Fatal(err) } if count != 1 { t.Fatalf("got %d orders, want 1", count) } }

et.NewTestDatabase clones a template database that already has the migrations applied and drops it at the end of the test, and it returns its own *sqldb.Database, which is why insertOrder takes the database as a parameter. In Go, et.MockEndpoint replaces a single endpoint, such as one that calls a payment provider, for one test without affecting tests running in parallel. Traces for every test request show up in the local dashboard at localhost:9400, and the local MCP server lets an agent read them when a test fails.

Key features

  • encore test starts the declared databases and Pub/Sub in test mode for TypeScript and Go.
  • Per-test databases in Go with et.NewTestDatabase, cloned from a migrated template.
  • Assertions on published messages with et.Topic(...).PublishedMessages() in Go.
  • Endpoint and service mocks in Go that are scoped to one test.
  • The same declarations are provisioned in your own AWS or GCP account on deploy, and in a preview environment per pull request.

Good to know

Tests share one database by default for speed, so tests that write data should use unique values or, in Go, their own database. Go tests must run through encore test instead of go test, because Encore adds a compilation step, and TypeScript tests need the test script in package.json pointing at the runner. Put that command in the agent's rules file, which encore llm-rules init generates for Cursor, Claude Code, VS Code or Zed, or as an AGENTS.md file.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

Testcontainers: a database container per suite

Testcontainers

Testcontainers starts real services in containers from test code, and its Postgres module returns a connection string the suite passes to the app.

import { PostgreSqlContainer, type StartedPostgreSqlContainer, } from "@testcontainers/postgresql"; import { afterAll, beforeAll } from "vitest"; let container: StartedPostgreSqlContainer; beforeAll(async () => { container = await new PostgreSqlContainer("postgres:16").start(); process.env.DATABASE_URL = container.getConnectionUri(); // run migrations against DATABASE_URL here }); afterAll(async () => { await container.stop(); });

Key features

  • Real Postgres, Redis, Kafka and other services in tests.
  • Libraries for Node.js, Go, Java, Python and more.
  • Each suite gets a clean instance.

Good to know

The agent has to wire the connection string into the app, run migrations and start a broker for any queue, in every suite that needs them, and that setup drifts from production configuration over time. Starting containers adds seconds per suite, so per-test isolation is usually traded away.

Shared Docker Compose database

A single Postgres from a Compose file is the most common setup, and it catches SQL errors as long as migrations run first. Shared state is the weak point, since tests that the agent adds write rows that other tests read, and the fix is truncation between tests, which the agent has to remember in each new file. Running two agent sessions at once against the same database makes it worse, which is covered in how to give AI coding agents real infrastructure to test against.

Mocks

Mocking the database with something like vi.mock in Vitest or an interface in Go makes tests fast and deterministic, and it means the test encodes the same assumptions the agent made when it wrote the code. Keep mocks for external services you do not control, and test everything that touches your own database and queues against the real thing, which is the kind of check Anthropic recommends giving an agent so it can verify its own work.

How to choose

  • Encore when the backend is TypeScript or Go and you want one command that runs tests against real Postgres and Pub/Sub, with per-test databases and message assertions.
  • Testcontainers when you are on another framework and want real services per suite.
  • Shared Docker Compose database for small projects with one developer and strict cleanup.
  • Mocks for third-party APIs only.

Whichever setup you use, write the test command into the agent's rules file and ask it to run the tests before every commit, since a check the agent never runs does not help. The Go testing package docs cover the standard flags that encore test passes through.

Frequently asked questions

How do I test a backend written by an AI coding agent?

Give the agent one command that runs integration tests against a real database and real queues, and ask it to run that command before it reports a change as done. Tests should call the API endpoints, then check the rows in Postgres and the messages published to Pub/Sub, rather than mocking the database. With Encore, encore test sets up separate test databases and local Pub/Sub for TypeScript and Go apps, Go tests can request a fresh, fully migrated database per test with et.NewTestDatabase, and et.Topic lets a test assert on the messages a handler published.

Should AI-generated backend code be tested with mocks?

Mostly not. A mock checks that the code calls an interface the way the test expects, which is the same assumption the agent made when writing the code, so a wrong SQL query or a missing migration passes. Mocks are useful for external APIs you do not control, such as a payment provider.

How do I keep agent-written tests from interfering with each other?

Give tests that write data their own database. Encore reuses one test database by default for speed, and in Go et.NewTestDatabase clones a migrated template database for a single test and drops it afterwards. With Testcontainers you start a container per suite, and with a shared Docker Compose database you have to truncate tables between tests.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders