# Cheat sheet — Unit testing (this repo)

Dense revision sheet. One glance per lesson. Interview one-liners at the bottom.

---

## The house test skeleton (memorize this shape)

```go
func TestThing_Method(t *testing.T) {
    t.Parallel()                                                   // always
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)  // always
    defer cancel()

    type testSuite struct {
        sut      *ThingImpl
        db       *mock_database.Ext
        tx       *mock_database.Tx
        mockRepo *mock_postgres.MockMyRepo
    }
    newSuite := func() *testSuite {                                // fresh mocks per case
        db, tx, r := new(mock_database.Ext), new(mock_database.Tx), new(mock_postgres.MockMyRepo)
        return &testSuite{sut: &ThingImpl{DB: db, Repo: r}, db: db, tx: tx, mockRepo: r}
    }
    assertExpected := func(t *testing.T, s *testSuite) {
        mock.AssertExpectationsForObjects(t, s.db, s.tx, s.mockRepo)
    }

    t.Run("return error when repo fails", func(t *testing.T) {
        // arrange
        s := newSuite()
        s.mockRepo.On("Get", ctx, mock.Anything).Return(nil, errFoo)
        // act
        err := s.sut.Method(ctx, ...)
        // assert
        assert.ErrorIs(t, err, errFoo)
        assertExpected(t, s)
    })
}
```
Sub-test names: **`"return <result> when <condition>"`**. One assertion per behaviour, no branches.

## The coverage checklist (per function)

```
1 error from every external call (repo/DB/downstream)   4 happy create
2 nil / not-found from repos                             5 happy update
3 guard / early-return conditions                        6 tx commit/rollback failures
```
Soft target: **>80% case coverage per function** (go-test-style.md:14).

## testify — mock

```go
m.On("Method", ctx, arg).Return(val, nil).Once()          // program (once)
m.On("Save", mock.MatchedBy(func(e *model.Email) bool {   // assert on arg CONTENTS
    return e.Status.String == "CREATED" && e.EmailID.String != ""
})).Return(nil)
m.On("Exec", ctx, mock.AnythingOfType("string"), mock.Anything).Return(tag, nil)  // wildcards
mock.AssertExpectationsForObjects(t, m1, m2)              // verify all programmed calls happened
m.On("X").Return(...).Run(func(a mock.Arguments) { ... }) // inspect/mutate args
```

## testify — assert vs require

| | fatal? | use for |
|---|---|---|
| `assert.*` | no (continues) | test bodies — `Equal`, `Nil`, `NotEmpty`, `Len`, `ErrorIs` |
| `require.*` | yes (`FailNow`) | helpers, where continuing is pointless |

## Mocking the database (usecase test)

```go
mockDB := new(mock_database.Ext);  mockTx := new(mock_database.Tx)
handler := CreateEmailHandler{ DB: mockDB, EmailRepo: mockEmailRepo }   // inject mocks into fields
mockDB.On("Begin", ctx).Return(mockTx, nil)     // transaction…
mockTx.On("Commit", ctx).Return(nil)            // …commits
```

## Mocking the database (repository test) — `testutil.MockDB`

```go
mockDB := testutil.NewMockDB()
mockDB.MockQueryArgs(t, nil, ctx, mock.Anything, database.TextArray(ids))   // set up Query
mockDB.MockScanArray(nil, fields, [][]interface{}{ row1, row2 })            // fake multi-row Scan
// error cases: pass an err to MockQueryArgs, or pgx.ErrNoRows to the scan
repo.On("Exec", ...)  // or mockDB.MockExecArgs(t, pgconn.CommandTag("1"), nil, params...)
```
`MockScanArray` copies expected values into the caller's scan destinations via reflection — no DB.
`testutil.ParseSQL` can assert on the emitted **SQL shape** (`pg_query_go`).

## Generating mocks

```bash
make service=spike gen-mock-service     # regenerate one service's mocks
make gen-mock-v2                        # eureka_v2
# after changing ANY interface → regenerate, or the build breaks
```
Two generators: **structs → reflection codegen** (`GenMockStructs`, `mock_struct.go`) ·
**interfaces → mockery wrapper** (`GenMockInterfaces`, `--with-expecter`). Both → testify mocks in `mock/`.
(Reflection mocks carry a misleading `// Code generated by mockgen` header — it's NOT gomock.)

## Running & coverage

```bash
go test ./internal/spike/modules/email/... -run ^TestHandler_CreateEmail$ -v   # one test
make service=spike unit-test-service     # whole service, -count=3, coverage total printed
```
`-count=3` = run 3× (flake/order detection → tests must be deterministic + parallel-safe).
Coverage profiles strip generated code (`_generated_impl.go`, `.sql.go`).

## Testing by layer (mock the layer below)

| Layer | Test file | Mock |
|---|---|---|
| repository | `.../repositories/*_test.go` | `testutil.MockDB` / `mock_database.Ext` (no real DB) |
| usecase/handler | `.../application/**/*_test.go` | the **repo interfaces** + `mock_database.Ext/Tx` |
| gRPC controller | `.../controller/grpc/*_test.go` | the **usecase interface** (+ kafka/db mocks) |

## Scope: unit ONLY

Excluded (BDD/E2E): `features/`, `internal/gandalf`, `local/run.bash bdd` (godog + Docker + real DB),
repo tests hitting a real database. Teach only `go test ./internal/...` + testify + `mock/`.

---

## Interview one-liners

- **"How do you structure a Go test?"** — table-driven: one subtest per case (`t.Run`), each with
  `arrange/act/assert`, a fresh suite, `t.Parallel`, and a `context.WithTimeout`. Sub-test names read
  "return X when Y."
- **"Mock vs stub?"** — a stub answers queries with canned data; a mock is pre-programmed with
  expectations it *verifies*. Ours are true mocks — `AssertExpectationsForObjects` fails if a
  programmed call never happened.
- **"How do you mock the database?"** — we don't hit a DB in unit tests. We mock `database.Ext`/`Tx`,
  and for repos a house `testutil.MockDB` fakes pgx `Scan` (reflection into the scan targets) and even
  parses the SQL. Integration/E2E use a real DB, separately.
- **"How are mocks generated?"** — a custom `gen-mock` CLI: concrete structs via a reflection codegen,
  interfaces via mockery. Both produce testify mocks; regenerate after any interface change.
- **"What do you assert?"** — behaviour, not internals: the returned value/error, and that the right
  collaborator calls happened with the right args (`MatchedBy`). Cover every error path.
- **"Why `-count=3`?"** — to catch flaky/order-dependent tests. It forces determinism and
  parallel-safety, which is why every test gets a fresh suite and no shared state.
