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:
encore test with real infrastructureEncore 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;
},
);

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.
encore test starts the declared databases and Pub/Sub in test mode for TypeScript and Go.et.NewTestDatabase, cloned from a migrated template.et.Topic(...).PublishedMessages() in Go.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.
Run in your terminal to get started locally.
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();
});
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.
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.
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.
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.
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.
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.
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.