Ah yes, AI, our favorite topic. Give an agent an empty folder and it can have an API and test suite running before I've finished reading the plan. I like the speed. I get nervous when all that code wraps around decisions the team didn't make.
Testing is where I keep getting hung up. I can ask an agent to “add tests for OrderService” and get a green suite, but that prompt says nothing about our architecture or where we draw the test boundary. It doesn't tell the agent how we use mocks either. If I leave those choices out, I've handed them to the tool.
The result can look fine in a PR. Each dependency has a mock and coverage goes up. Then somebody renames an internal method and half the suite fails while the application behaves exactly as it did before. We got tests, though.
Coverage gave us a number
Teams had this problem before AI. I've seen engineers write tests to reach a coverage threshold because the pipeline asked for 80%. The target is clear. Make the line run, cover the other branch, get CI green.
An 80% line coverage gate tells me which code ran during the suite. It doesn't tell me whether an order can ship twice. A test that calls every getter can raise the number without giving me much confidence in anything.
I understand why people chase it. Test setup takes too long, fixtures break when a type changes, and some integration suites give you enough time to reconsider your career. Add a flaky failure and the team learns to click rerun before anyone reads what broke.
AI can take a decent bite out of that work. It can build the fixture or clean up repeated setup. It can also generate forty shallow tests and reach 80% before lunch, which puts us back where we started.
Before we talk about 80%, tell me which bug this test stops and whether it still works after we move the code around.
The prompt needs the architecture
Alistair Cockburn’s original description of hexagonal architecture puts the application behind ports, with adapters connecting the UI or database. If placeOrder is one of those ports, I want the test driving it instead of documenting every method call behind it.
An agent looking at a service with two dependencies may produce this:
it("places an order", async () => {
const inventory = {
reserve: vi.fn().mockResolvedValue(true),
};
const orders = {
save: vi.fn().mockResolvedValue(undefined),
};
const service = new OrderService(inventory, orders);
await service.place(order);
expect(inventory.reserve).toHaveBeenCalledWith(order.items);
expect(orders.save).toHaveBeenCalledWith(order);
});
This test checks which collaborators the service calls. That may be the contract the team cares about. If it isn't, we just tied the test to today's implementation and made the next refactor louder.
If the team treats placeOrder as an application port, I may want to drive that port with an in-memory adapter and check the result:
it("rejects an order when stock is gone", async () => {
const inventory = new InMemoryInventory({
sku: "watch-01",
quantity: 0,
});
const app = createOrderApp({ inventory });
const result = await app.placeOrder({
sku: "watch-01",
quantity: 1,
});
expect(result).toEqual({
ok: false,
reason: "out_of_stock",
});
});
I prefer this version when the port is the contract. The test knows less about the methods behind it and more about the behavior I want to keep. The agent can't pull “hexagonal architecture” out of thin air, though.
If the repository doesn't show a clear boundary or include example tests, I need to put that context in the prompt. Better autocomplete won't teach an engineer which boundary the team cares about.
“Use TDD” doesn't settle much
Kent Beck describes TDD as a loop where the programmer chooses the next test, sees it fail, changes the code, and refactors. Two engineers can follow that loop and end up with different tests because they didn't choose the same boundary.
In Mocks Aren’t Stubs, Martin Fowler describes classical and mockist TDD. One style reaches for real collaborators when they're cheap to use. The other pays closer attention to messages between objects and uses mocks to check them.
Mocks change the design. So do slow integration tests. If I tell the agent “use TDD” and walk away, it will copy whichever mess is already in the repo.
Maybe that pattern is intentional. I want the engineer to know before the agent adds another test to it.
I want AI doing the setup I hate
I want AI doing the setup I hate and poking holes in the cases I picked. I don't want it deciding why the test exists.
Once I know the behavior and the boundary, I can give the agent an acceptance rule and ask it to build the fixture. For a bug, I can give it the failing input and ask for a characterization test before touching the fix. That saves me time without asking the tool to invent the contract.
I use it to challenge my test too. Date boundaries are good for this because I will miss one and feel clever about the cases I did remember. I can also ask whether a mock couples the test to an implementation detail or whether an in-memory adapter could replace the database setup.
The generated test still needs to fail for the expected reason before the implementation changes. A broken fixture doesn't validate the bug. If the test stays green after removing the fix, we automated nothing.
The PR conversation is pretty short
I care less about which lines came from a model than whether the author can explain the test. Which behavior does it protect? Start there. “Coverage was low” tells me how the work started, but we have more to talk through before I approve it.
If the author can't explain why the test uses a mock or crosses a database boundary, we pause before generating another file. The agent can wait.
AI can type most of the test. The person opening the PR still needs to know why we should keep it.