TATD: Test-Driven AI Development in Golang — The Red-Green-Refactor Workflow with AI
Test-Driven AI Development for Go: a tests-first workflow with AI that produces high-quality code. Red-Green-Refactor with Claude Code, Cursor, and other tools.
Test-Driven AI Development (TATD) for Golang
Test-Driven AI Development (TATD) for Golang is the combination of two proven practices: Test-Driven Development (TDD), which raises code quality, and AI, which accelerates implementation. The result is AI’s speed plus TDD’s quality — code that genuinely satisfies every acceptance criterion without the slowness of writing tests by hand that has always limited TDD adoption.
12.1 Why TDD + AI Is a Powerful Combination
To understand the value of TATD, we need to look at the shortcomings of each approach in isolation. The comparison below sets traditional TDD, AI coding without TDD, and TATD side by side.
1Traditional TDD:
2 Red (write failing test) → Green (implement) → Refactor
3
4 Pros: forced clarity about behavior, high test coverage
5 Cons: SLOW — writing the test first feels like overhead
6 Adoption: low because the perceived cost is high
7
8Traditional AI Coding (no TDD):
9 Prompt → AI generates code → developer reviews
10
11 Pros: FAST
12 Cons: low coverage, ambiguous behavior, hard to verify correctness
13
14TATD (Test-Driven AI Development):
15 Spec → AI writes tests → Review tests → AI implements → Verify
16
17 Pros: AI speed + TDD quality + forced spec clarity
18 Cons: more upfront work (worth it for production code)The key is in the last line: TATD moves TDD’s biggest friction (writing slow tests by hand) onto the AI, while keeping the “spec first, then code” discipline.
12.2 The TATD Workflow: Step by Step
The TATD flow is easiest to grasp through a concrete example. The eight steps below trace the CancelOrder feature from spec to verification, with AI writing the tests first.
1# Step 1: Start from a spec or a task
2# (Can be from .specify/features/x/spec.md or a task description)
3
4Spec: CancelOrder — A customer can cancel a PENDING order within 30 minutes
5
6# Step 2: AI writes the tests FIRST
7claude
8> Read this spec:
9> ---
10> Feature: CancelOrder
11> AC1: Order PENDING + created < 30 minutes ago → can be cancelled → 204
12> AC2: Order not PENDING → error ORDER_NOT_CANCELLABLE → 409
13> AC3: Order PENDING + created > 30 minutes ago → error CANCEL_WINDOW_EXPIRED → 409
14> AC4: Order does not belong to the requesting user → 404 ORDER_NOT_FOUND
15> AC5: Stock is restored atomically
16> AC6: A Kafka event ORDER_CANCELLED is sent
17> ---
18>
19> Write unit tests for CancelOrderUseCase that cover ALL of the ACs.
20> Pattern: testify/suite + gomock as per CLAUDE.md.
21> DO NOT implement the usecase yet — tests only.
22> The tests must be in a FAILING (red) state.
23
24# Step 3: Review the generated tests
25# Verify:
26# - Do all the ACs have test coverage?
27# - Is the test setup correct (mock, suite)?
28# - Do the test assertions make sense?
29
30# Step 4: Run to confirm they fail
31go test ./internal/usecase/order/... -run TestCancelOrder
32# Expected: compilation error or test failures
33
34# Step 5: AI implements to make the tests pass
35claude
36> Implement CancelOrderUseCase so that ALL tests pass.
37> The test file already exists in cancel_order_test.go — do not change it.
38> Only create cancel_order.go with the correct implementation.
39
40# Step 6: Run the tests
41go test ./internal/usecase/order/... -race
42# Expected: ALL PASS
43
44# Step 7: Refactor if needed
45claude
46> Is there anything that can be refactored in this implementation
47> without changing behavior? All tests must still pass.
48# Run the tests again after refactoring
49
50# Step 8: Extend to the next layer (repository, handler)
51# Same process: write test → implement → verifyNotice Step 5, which explicitly states “the test file already exists — do not change it”: this is the core safeguard of TATD, forcing the AI to make the implementation pass the spec, not the other way around.
12.3 A Prompt Template for TATD: Tests-First
The quality of the generated tests depends heavily on the prompt. The three templates below cover the most common scenarios: from a spec, from a signature, and from existing code.
1The most effective prompts for generating tests:
2
3Template A: From a spec (most structured)
4"Write unit tests for [UseCaseName] that cover all of these ACs:
5@specs/order/cancel-order.md (AC1-AC10)
6
7Requirements:
8- Pattern: testify/suite + gomock (as per CLAUDE.md)
9- Each AC = at least 1 test function
10- Include edge cases: concurrent scenario, nil inputs
11- Tests must be FAILING right now (before implementation)
12- File: internal/usecase/[domain]/[feature]_test.go"
13
14Template B: From a function signature (backward)
15"This is the function I'm about to implement:
16 func (uc *CancelOrderUseCase) Execute(ctx context.Context, input CancelOrderInput) error
17
18Input: CancelOrderInput{OrderID uuid.UUID, UserID uuid.UUID}
19
20Behavioral expectations:
21- Happy path: PENDING order within 30 minutes → nil error
22- Not found: nil, nil from repo → ErrOrderNotFound
23- Wrong status: not PENDING → OrderNotCancellableError
24- Timeout: created > 30 minutes ago → ErrCancelWindowExpired
25
26Write comprehensive tests before I implement."
27
28Template C: From an existing implementation (coverage improvement)
29"Here is an implementation that already exists:
30@internal/usecase/order/cancel_order.go
31
32Identify all scenarios that aren't yet covered by tests,
33then write tests for better coverage."All three share one principle: name the pattern (testify/suite + gomock) and explicitly ask for edge cases — without that, the AI tends to write shallow tests that only exercise the happy path.
12.4 TATD for the Repository Layer
Repository tests need a different approach because they touch a real database. The Go example below shows the integration-test pattern with the integration build tag that separates it from unit tests.
1// Two approaches for repository tests:
2
3// APPROACH 1: Interface mock (unit test, recommended)
4// Mock the repository to test the usecase that depends on it
5// → No real DB needed
6// → Fast, isolated
7
8// APPROACH 2: Real DB (integration test)
9// To test the repository implementation itself
10// → Needs a test database (Docker compose)
11// → Slower but a more comprehensive test
12
13// Integration test pattern with a build tag:
14//go:build integration
15
16package postgres_test
17
18import (
19 "context"
20 "testing"
21 "github.com/stretchr/testify/suite"
22)
23
24type OrderRepositoryIntegrationSuite struct {
25 suite.Suite
26 db *pgxpool.Pool
27 repo *orderRepository
28}
29
30func (s *OrderRepositoryIntegrationSuite) SetupSuite() {
31 // Connect to the test DB (from env var)
32 dbURL := os.Getenv("TEST_DATABASE_URL")
33 pool, err := pgxpool.New(context.Background(), dbURL)
34 s.Require().NoError(err)
35 s.db = pool
36 s.repo = &orderRepository{db: pool}
37}
38
39func (s *OrderRepositoryIntegrationSuite) TearDownSuite() {
40 s.db.Close()
41}
42
43func (s *OrderRepositoryIntegrationSuite) SetupTest() {
44 // Clean tables before each test
45 _, err := s.db.Exec(context.Background(),
46 "TRUNCATE orders, order_items, inventory RESTART IDENTITY CASCADE")
47 s.Require().NoError(err)
48}
49
50func (s *OrderRepositoryIntegrationSuite) TestCancelWithStockRestore_HappyPath() {
51 // Arrange: create order in DB
52 orderID := s.createTestOrder(domain.StatusPending)
53
54 // Act
55 err := s.repo.CancelWithStockRestore(context.Background(), orderID)
56
57 // Assert
58 s.NoError(err)
59 order, err := s.repo.GetByID(context.Background(), orderID)
60 s.NoError(err)
61 s.Equal(domain.StatusCancelled, order.Status)
62 // Verify stock restored
63}
64
65func TestOrderRepositoryIntegration(t *testing.T) {
66 suite.Run(t, new(OrderRepositoryIntegrationSuite))
67}Notice the //go:build integration tag and the SetupTest that TRUNCATEs the tables: this pattern keeps the test deterministic while also letting it be excluded from a normal unit-test run.
To run both kinds of test separately, use the build-tag flag as shown below.
1# Run integration tests:
2go test -tags=integration ./internal/repository/postgres/... -v
3
4# Run unit tests only (no integration):
5go test ./internal/repository/postgres/...
6# (integration tests excluded by the build tag)With the build tag, unit tests stay fast on every commit while the slow integration tests run only when needed — a separation that matters so TATD doesn’t slow down the daily loop.
12.5 AI-Generated Tests: Common Issues and Fixes
Issue 1: Tests that don’t really test behavior
The most common problem: the AI produces a test that passes but doesn’t test anything. Compare the trivial test below with the version that genuinely enforces specific behavior.
1// AI generates this (too trivial):
2func (s *CancelOrderSuite) TestExecute() {
3 err := s.uc.Execute(context.Background(), CancelOrderInput{
4 OrderID: uuid.New(),
5 UserID: uuid.New(),
6 })
7 // No assertions about specific behavior!
8 s.NoError(err)
9}
10
11// What it should be:
12func (s *CancelOrderSuite) TestExecute_PendingOrder_WithinWindow_Success() {
13 // Arrange: specific state
14 orderID := uuid.New()
15 order := &domain.Order{
16 ID: orderID,
17 UserID: s.userID,
18 Status: domain.StatusPending,
19 CreatedAt: time.Now().Add(-10 * time.Minute), // within the 30-min window
20 }
21 s.mockRepo.EXPECT().
22 GetByIDAndUserID(gomock.Any(), orderID, s.userID).
23 Return(order, nil)
24 s.mockRepo.EXPECT().
25 CancelWithStockRestore(gomock.Any(), orderID).
26 Return(nil)
27
28 // Act
29 err := s.uc.Execute(context.Background(), CancelOrderInput{
30 OrderID: orderID,
31 UserID: s.userID,
32 })
33
34 // Assert: specific behavior
35 s.NoError(err)
36 // If Kafka: also assert the event was published
37}The difference: the correct version sets up specific state (a PENDING order within the 30-minute window) and configures the mock expectations — so when the test turns green, you know the exact behavior has been satisfied.
Issue 2: Missing error cases
Generated tests often cover only the happy path. The prompt below explicitly forces coverage of every error scenario.
1A better prompt for comprehensive error coverage:
2"Write tests that cover ALL scenarios including:
3- Happy path
4- Not found (nil, nil from the repo)
5- Permission check (order belongs to another user)
6- Business rule violations (status, time window)
7- Infrastructure errors (DB error, Kafka error)
8- Concurrent scenarios
9- Nil input handling"Explicitly listing the scenarios is the cheapest way to raise coverage — the AI rarely guesses error cases on its own unless asked.
Issue 3: Wrong mock setup
A subtle but frequent mistake: the AI mixes testify/mock style with gomock. The example below shows the wrong form and the correct one.
1// AI generates (wrong):
2s.mockRepo.On("GetByIDAndUserID", mock.Anything, mock.Anything, mock.Anything).
3 Return(order, nil)
4// testify/mock style, not gomock!
5
6// Correct (gomock style):
7s.mockRepo.EXPECT().
8 GetByIDAndUserID(gomock.Any(), gomock.Any(), gomock.Any()).
9 Return(order, nil)The fix is structural: make sure CLAUDE.md contains an exact mock-setup example with gomock, so the AI doesn’t fall back to the incompatible testify/mock style.
12.6 TATD Metrics: How to Measure Improvement
TATD adoption should be measured, not merely felt. The five metrics below are the most useful to track per sprint to prove its impact.
1Metrics worth tracking after adopting TATD:
2
31. Test Coverage:
4 go test -cover ./...
5 Target: > 80% for the usecase layer
6
7 Before TATD adoption: [baseline]
8 After 1 month: [new number]
9
102. Time to PR:
11 From task start to PR ready
12 Before: [baseline]
13 After: should go up (more confident, less rework)
14
153. PR Review Comments (mechanical):
16 Comments like "missing error check", "wrong error handling"
17 Before: [baseline]
18 After: should drop significantly
19
204. Post-deploy bugs:
21 Bugs discovered after deployment
22 Before: [baseline]
23 After: should drop (better coverage)
24
255. Developer Confidence:
26 A simple survey: "how confident are you in the code you push?"
27 Scale 1-10
28 Before: [baseline]
29 After: should go up
30
31Track all of these per sprint, evaluate after 3 months.The most convincing metrics for management are numbers 3 and 4 — a drop in mechanical PR review comments and post-deploy bugs translates TATD directly into cost savings.
12.7 TATD with Multiple Tools
The TATD experience differs from tool to tool. The summary below maps the best way to run TATD in Claude Code, Cursor, Copilot, and Windsurf.
1TATD approach per tool:
2
3Claude Code (best TATD experience):
4 - Plan mode to design tests before writing
5 - Can iterate on its own: write test → run → fix → repeat
6 - Can verify that tests are genuinely failing before implementing
7
8 Optimal flow:
9 > "Write tests for [feature]. Run them and confirm they fail.
10 Then implement to make them pass. Then run again to confirm they pass."
11 Claude handles all the steps automatically.
12
13Cursor (good TATD experience):
14 - Composer: "Write the test file first, then the implementation"
15 - The visual diff helps a lot when reviewing test quality
16 - Agent Mode: can run tests between steps
17
18 Optimal flow:
19 Composer step 1: "tests only" → review diff → accept
20 Composer step 2: "implementation only" → review diff → accept
21 Terminal: go test
22
23Copilot (adequate TATD):
24 - /tests command from a function signature
25 - Manual workflow (not autonomous)
26 - Good for adding test coverage to existing code
27
28Windsurf (good for TATD via Flows):
29 - Set up a Flow: write-test → verify-failing → implement → verify-passing
30 - Cascade helps maintain test context across sessionsClaude Code stands out because it can close the write→run→fix loop autonomously; other tools are still capable of TATD but demand more manual steps from you.
12.8 Red-Green-Refactor with AI: A Healthy Cycle
The classic Red-Green-Refactor cycle remains the heart of TATD; AI simply speeds up each phase. The breakdown below shows the time estimate and commit for each phase.
1A healthy TATD cycle:
2
3RED Phase (5-10 minutes):
4 AI: "Write comprehensive tests for [feature]"
5 → Review the tests: do they cover all scenarios?
6 → Run: go test → confirm FAIL
7 → Commit: git commit -m "test: add tests for cancel order [failing]"
8
9GREEN Phase (10-20 minutes):
10 AI: "Implement to make all tests pass.
11 Do not change the test files."
12 → Run: go test → confirm PASS
13 → Commit: git commit -m "feat: implement cancel order"
14
15REFACTOR Phase (5-10 minutes):
16 AI: "Are there any refactoring opportunities?
17 All tests must still pass."
18 → Run: go test -race → confirm still PASS
19 → Commit: git commit -m "refactor: simplify cancel order logic"
20
21Total for a standard feature: 20-40 minutes
22vs traditional (no TDD, no AI): 60-120 minutes with more bugsThe total answers the most common objection: 20-40 minutes with TATD against 60-120 minutes the traditional way — TATD is actually faster while producing fewer bugs.
12.9 TATD for Hotfixes and Bug Fixes
TATD isn’t only for new features; it is highly effective for bug fixes. The flow below shows how to write a test that reproduces the bug first, then fix it.
1TATD is also very effective for bug fixes:
2
3Bug report: "The order total amount is wrong when an item is discounted"
4
5TATD approach:
61. AI: "Write a test that reproduces this bug:
7 Order with a discounted item → wrong total amount
8 The test must FAIL with the current behavior"
9 → AI generates a failing test
10
112. Review the test: does it really capture the bug?
12 Run: go test → confirm FAIL
13
143. AI: "Fix the bug without changing the test.
15 The test is the spec of the expected behavior."
16 → AI fixes the implementation
17
184. Run: go test → confirm PASS
19
205. Add the test to the regression suite
21 "This test will prevent this bug from reappearing"
22
23The non-obvious value:
24 Bug fix without a test = the bug can come back (regression)
25 Bug fix with a test = permanent prevention
26 TATD forces you to create a regression test, which is the most valuableThe hidden value: writing the failing test first forces every bug fix to produce a regression test — permanent prevention so the same bug never appears again.
12.10 Advanced: Property-Based Testing with AI
For domains with mathematical rules, property-based testing can be far more powerful than example-based tests. The Go code below uses the gopter library to verify the properties of a price calculation against thousands of random inputs.
1// Install: go get github.com/leanovate/gopter
2
3// AI generates a property-based test for the price calculation:
4
5import (
6 "github.com/leanovate/gopter"
7 "github.com/leanovate/gopter/gen"
8 "github.com/leanovate/gopter/prop"
9)
10
11func TestPriceCalculation_Properties(t *testing.T) {
12 properties := gopter.NewProperties(nil)
13
14 properties.Property("subtotal never negative", prop.ForAll(
15 func(price int64, qty int) bool {
16 // Property: subtotal must always be >= 0
17 if price < 0 || qty <= 0 {
18 return true // skip invalid inputs
19 }
20 result := CalculateSubtotal(price, qty)
21 return result >= 0
22 },
23 gen.Int64Range(0, 1_000_000_000), // price 0 - 10 million rupiah
24 gen.IntRange(1, 100), // quantity 1-100
25 ))
26
27 properties.Property("discount never exceeds subtotal", prop.ForAll(
28 func(price int64, qty int, discountPct int) bool {
29 if price <= 0 || qty <= 0 || discountPct < 0 || discountPct > 100 {
30 return true
31 }
32 subtotal := CalculateSubtotal(price, qty)
33 discount := ApplyDiscount(subtotal, discountPct)
34 return discount <= subtotal
35 },
36 gen.Int64Range(1, 1_000_000_000),
37 gen.IntRange(1, 100),
38 gen.IntRange(0, 100),
39 ))
40
41 properties.TestingRun(t)
42}The power of this approach: instead of guessing a few examples, gopter executes hundreds of random combinations to test universal properties like “discount never exceeds subtotal”.
To get tests like this from the AI, steer it to identify the mathematical properties first with the prompt below.
1# Prompt to the AI for property-based tests:
2claude
3> Domain: OrderPricing (calculation of total, discount, tax)
4>
5> Identify all mathematical properties that must always hold:
6> for example: total >= 0, discount <= subtotal, tax >= 0, etc.
7>
8> Generate property-based tests using the gopter library
9> that verify all of these properties with random input.Asking the AI to list the properties before writing the tests produces far more thorough coverage than immediately asking it to “write tests”.
12.11 TATD Integration with CI/CD
So that TATD discipline doesn’t depend on developer memory, enforce it through the pipeline. The GitHub Actions workflow below runs unit tests, integration tests, and an 80% coverage gate for the usecase layer.
1# .github/workflows/tatd-quality.yml
2name: TATD Quality Gates
3
4on: [push, pull_request]
5
6jobs:
7 test-quality:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11 - uses: actions/setup-go@v5
12 with:
13 go-version: '1.22'
14
15 - name: Unit Tests (required)
16 run: go test -race -count=1 ./...
17
18 - name: Integration Tests (optional, non-blocking)
19 run: go test -tags=integration -race ./... || true
20 env:
21 TEST_DATABASE_URL: ${{ secrets.TEST_DB_URL }}
22
23 - name: Coverage Report
24 run: |
25 go test -coverprofile=coverage.out ./...
26 go tool cover -func=coverage.out
27
28 # Fail if usecase coverage < 80%
29 USECASE_COVERAGE=$(go tool cover -func=coverage.out | \
30 grep "internal/usecase" | \
31 awk '{print $3}' | \
32 tr -d '%' | \
33 sort -n | head -1)
34
35 if (( $(echo "$USECASE_COVERAGE < 80" | bc -l) )); then
36 echo "Usecase coverage ${USECASE_COVERAGE}% < 80% threshold"
37 exit 1
38 fi
39 echo "Usecase coverage: ${USECASE_COVERAGE}%"This coverage gate is what turns TATD from good intentions into an enforced standard: a PR that drops usecase coverage below 80% fails automatically before it can be merged.
12.12 Tips & Gotchas
Tip 1: Tests as spec verification
Before approving the AI’s tests, read each test function and ask: “If this test passes, does that mean this AC is satisfied?” If in doubt, refine the test before moving on.
Tip 2: One AC = at least one test
Count the ACs in the spec, count the test functions. If there are fewer tests than ACs, something was missed.
Tip 3: Strict mock assertions
Use gomock.InOrder() to verify that calls happen in the correct order for a sequential workflow.
Tip 4: Table-driven tests for large combinations
For many combinations of status or input, a table-driven test is far more concise. The example below tests the entire order-status matrix in one function.
1func (s *CancelOrderSuite) TestCancelOrder_StatusMatrix() {
2 testCases := []struct {
3 name string
4 orderStatus domain.OrderStatus
5 expectedErr error
6 }{
7 {"pending", domain.StatusPending, nil},
8 {"confirmed", domain.StatusConfirmed, &domain.OrderNotCancellableError{}},
9 {"shipped", domain.StatusShipped, &domain.OrderNotCancellableError{}},
10 {"cancelled", domain.StatusCancelled, &domain.OrderNotCancellableError{}},
11 }
12
13 for _, tc := range testCases {
14 s.Run(tc.name, func() {
15 // test with tc.orderStatus and tc.expectedErr
16 })
17 }
18}This table-driven pattern is idiomatic in Go: adding a new case is just one line in the slice, so keeping the status-matrix coverage maintained stays easy.
Gotcha 1: AI that “fixes” the tests
When the implementation fails, the AI sometimes edits the test file (instead of the implementation) to make the test pass. The explicit instruction: “DO NOT change the test files.”
Gotcha 2: Mocks that are too permissive
gomock.Any() for every parameter = a test that doesn’t really test anything. Be specific: gomock.Eq(orderID) for an ID you already know.
12.13 Common Resistance and How to Overcome It
TATD adoption often faces predictable objections. The three common arguments below are answered with context on when the objection is valid and when it isn’t.
1"TATD is slower than implementing directly"
2→ True for small features (< 30 minutes).
3 For features that take > 1 hour: TATD is FASTER because:
4 - It reduces debugging time post-implementation
5 - It reduces rework from misunderstood requirements
6 - The tests catch regressions in future sprints
7
8"AI can generate tests + implementation at once"
9→ It can, but the result is often: tests that pass the implementation
10 rather than an implementation that passes the behavior spec.
11 Tests-first forces: spec → tests → impl (the correct order)
12
13"Not every developer is disciplined enough for TDD"
14→ TATD is easier because the AI "writes" the tests
15 The developer only needs to REVIEW, not write from scratch
16 The barrier to entry is much lowerThe strongest answer is in the third objection: precisely because the AI writes the tests, the discipline barrier of TDD collapses — the developer just reviews, a task far lighter than writing from scratch.
12.14 TATD for Golang Generics
Generics make TATD even more valuable because a generic utility must be correct across many types. The example below writes tests for a Result[T] type before the generic type itself is implemented.
1// With Go 1.22 generics, TATD is very helpful for generic utilities
2
3// Prompt:
4// "Write tests for the generic Result type we're about to build:
5//
6// Properties that must be tested:
7// - Result[T].Ok() returns true if there is no error
8// - Result[T].Err() returns the error if there is one
9// - Result[T].Value() panics if there is an error
10// - Result[T].ValueOrDefault(def) returns def if there is an error
11//
12// Generate the tests BEFORE I implement this generic type."
13
14// AI generates:
15func TestResult_OkAndErr(t *testing.T) {
16 t.Run("success result", func(t *testing.T) {
17 r := Ok[string]("hello")
18 assert.True(t, r.IsOk())
19 assert.NoError(t, r.Err())
20 assert.Equal(t, "hello", r.Value())
21 })
22
23 t.Run("error result", func(t *testing.T) {
24 r := Err[string](errors.New("something failed"))
25 assert.False(t, r.IsOk())
26 assert.Error(t, r.Err())
27 assert.Panics(t, func() { r.Value() })
28 })
29
30 t.Run("value or default on error", func(t *testing.T) {
31 r := Err[int](errors.New("err"))
32 assert.Equal(t, 42, r.ValueOrDefault(42))
33 })
34}
35// Tests first, then implement the generic typeBy writing the Result[T] contract as tests first, you force a clear generic API design before a single line of implementation is written — exactly the TATD principle applied to generics.
12.15 Summary
TATD is a highly valuable pattern for Go production development: Spec → AI tests → Review → AI implements → Verify.
Key benefits:
- AI’s speed plus TDD’s quality
- Forced spec clarity before implementation
- Built-in regression prevention
- Higher developer confidence
Adoption path: Start with one complex usecase in the next sprint. Track coverage and PR review comments, then evaluate after 2 sprints.
12.16 TATD for an Existing Codebase (Retrofit)
Not every project starts from scratch. The flow below shows how to apply TATD to an existing codebase, starting from identifying untested code.
1# Step 1: Identify untested code
2go test -coverprofile=coverage.out ./...
3go tool cover -html=coverage.out -o coverage.html
4# Open the browser: which parts are red (untested)?
5
6# Step 2: AI generates tests for the existing code
7claude
8> Analyze this function that has no tests yet:
9> @internal/delivery/http/handler/order_handler.go
10>
11> Identify all scenarios that should be tested,
12> then generate a comprehensive test suite.
13> Pattern: testify/suite + gomock.
14> Do not change the implementation.
15
16# Step 3: Fix the implementation if the tests reveal bugs
17# It happens often: when writing tests for existing code,
18# you find a bug that has already been in production!
19
20# Step 4: Commit the tests as a regression suite
21git add internal/usecase/order/cancel_order_test.go
22git commit -m "test: add missing test coverage for cancel order"The valuable side effect at Step 3: writing tests for old code often uncovers bugs that have long been in production — a TATD retrofit doubles as a quality audit.
12.17 TATD Checklist Per Feature
Before marking a feature “DONE”, verify its test coverage and quality. The checklist below separates test coverage, test quality, and integration.
1Before marking a feature "DONE":
2
3Test coverage:
4[ ] All ACs from the spec have test coverage
5[ ] A happy-path test exists
6[ ] A not-found scenario exists (nil, nil from the repo)
7[ ] A business-rule violation exists (e.g., status is not PENDING)
8[ ] An infrastructure error exists (DB error, Kafka error)
9[ ] A concurrent scenario exists if relevant
10[ ] All tests pass with go test -race
11
12Test quality:
13[ ] Each test has a meaningful name (Test[Feature]_[Scenario]_[Expected])
14[ ] Assertions are specific (not just s.NoError for everything)
15[ ] Mock expectations are specific (not all gomock.Any())
16[ ] Tests are independent (no shared state between tests)
17
18Integration:
19[ ] An integration test exists for the repository layer (if needed)
20[ ] The CI pipeline runs all tests
21[ ] The coverage gate passes (> 80% for the usecase layer)The “Test quality” section is the one most often neglected: high coverage without specific assertions only gives a false sense of security — make sure every test truly enforces behavior.
12.18 TATD vs BDD (Behavior-Driven Development)
TATD and BDD are often conflated, yet they differ in focus. The comparison below helps you choose between them, or combine them in a hybrid.
1TATD and BDD overlap but differ:
2
3BDD:
4 "Given [state], When [action], Then [outcome]"
5 More business-readable
6 Tools: godog (Cucumber for Go)
7
8TATD:
9 "Spec AC → failing Go test → implementation"
10 Developer-centric
11 Tools: testify/suite + gomock
12
13Choose TATD if:
14 - The team is engineers (no non-technical stakeholders)
15 - A Go codebase with Clean Architecture
16 - Speed is a concern (BDD is more verbose)
17
18Choose BDD if:
19 - A business analyst writes the scenarios
20 - Acceptance tests need to be read by non-engineers
21 - The project already uses Cucumber or the Gherkin format
22
23A hybrid approach that works:
24 - Spec Kit or Kiro for the spec (business-readable)
25 - TATD for the implementation tests (developer-centric)
26 - specify audit to verify alignmentFor a pure engineering team with Clean Architecture Go, TATD is usually the better fit; BDD only wins when there are non-technical stakeholders who need to read the acceptance tests.
12.19 Measuring TATD Success: A 30-Day Scorecard
To prove TATD’s impact concretely, use a 30-day scorecard. The format below compares the week-1 baseline against the week-4 results on key metrics.
1After 30 days of TATD adoption, track this:
2
3WEEK 1 baseline:
4 Usecase test coverage: ____%
5 PR mechanical review comments/PR: ____
6 Average time test → implementation: ____ minutes
7 Post-deploy bugs/month: ____
8
9WEEK 4 result:
10 Usecase test coverage: ____% (target: +20-30%)
11 PR mechanical review comments/PR: ____ (target: -40%)
12 Average time test → implementation: ____ minutes (target: similar or faster)
13 Post-deploy bugs/month: ____ (target: -50%)
14
15Developer satisfaction:
16 "How confident are you in the code you push?" (1-10)
17 Week 1 average: ____
18 Week 4 average: ____ (target: +2 points)This scorecard turns “it feels better” into measurable proof: targets like coverage +20-30% and post-deploy bugs -50% give management a concrete reason to continue the adoption.
12.20 Extended Summary
Test-Driven AI Development is the natural evolution of TDD in the AI era. AI removes TDD’s biggest friction (writing slow tests by hand) while preserving its benefits: forced spec clarity, high coverage, and regression prevention.
Three key habits to adopt:
- Always ask the AI to write the tests FIRST, confirm they fail, then implement
- Review every generated test — does it really test behavior, not the implementation?
- Track metrics every sprint — coverage, PR comments, post-deploy bugs
Bottom line: TATD is the way to get AI’s speed without sacrificing code quality. For Go production systems, it is a combination very much worth adopting.
12.21 Tooling to Support the TATD Workflow
To close, provide tooling so the TATD workflow is easy to run. The Makefile targets below wrap the test, coverage, and threshold-enforcement commands in one place.
1# Coverage enforcement via Makefile
2# Makefile
3
4test:
5 go test -race -count=1 ./...
6
7test-coverage:
8 go test -coverprofile=coverage.out ./...
9 go tool cover -func=coverage.out | grep "total:"
10
11test-coverage-check:
12 @COVERAGE=$$(go test -coverprofile=/tmp/cov.out ./... 2>/dev/null; \
13 go tool cover -func=/tmp/cov.out | \
14 grep "total:" | awk '{print $$3}' | tr -d '%'); \
15 if [ "$$(echo "$$COVERAGE < 75" | bc)" -eq 1 ]; then \
16 echo "FAIL: Coverage $$COVERAGE% < 75% threshold"; exit 1; \
17 else \
18 echo "PASS: Coverage $$COVERAGE%"; \
19 fi
20
21test-integration:
22 go test -tags=integration -race ./... -timeout 120s
23
24# Run before commit:
25pre-commit: test-coverage-check lintWith a pre-commit target that calls test-coverage-check, TATD discipline becomes a natural part of the daily workflow — not an extra step that’s easy to forget.