<!--
  Testing
  From the Iris book, chapter 16, covering Iris 14.0.0 on Go 1.27.
  Import path: github.com/kataras/iris/v14
  Canonical page: https://iris-go.com/docs/testing/
-->
# Chapter 16: Testing

Testing is where the architecture of this book pays off. Because an Iris
application is assembled by a `newApp` function that receives its
configuration and its dependencies as parameters, a test can build the real
application (real router, real middleware, real error map) and swap only
the edges: an in-memory store instead of a database, a fixed password
instead of a secret from the environment. The `httptest` package then
drives that application entirely in memory: no ports, no network, no
sleeps. A suite of full HTTP tests runs in milliseconds. This chapter
covers the `httptest` API, the request-and-assertion chain, designing
applications so they are testable in the first place, and the specific
tests every service should have: guards, validation, the central error
map, and the proof that internal errors never reach a client.

## Background

An automated test is a small Go function that runs a piece of your
application and checks the result against what you expect; the `go test`
command runs every such function and reports which ones failed. Its value
is not proving today's build works; you can click through the app for
that. Its value is that the proof re-runs on every future change. When a
change six months from now quietly breaks note creation, the suite says
so within seconds, names the exact expectation that no longer holds, and
points at the change that broke it. That failure mode (old behavior
broken by new code) is called a *regression*, and catching regressions
automatically is the reason test suites exist. Each individual check
inside a test ("the status must be 201") is an *assertion*.

Tests are usually named by how much of the system they exercise: a *unit
test* checks one function or type in isolation, an *integration test*
checks that several pieces work when wired together, and an *end-to-end
test* drives the assembled application the way a real client would. In
one breath: unit tests prove each part works, integration tests prove the
parts fit, end-to-end tests prove the whole does the job. This chapter
leans on end-to-end-style HTTP tests because in Iris they are as cheap as
unit tests, plus plain unit tests for domain logic.

Real applications depend on things a test does not want: a production
database, a mail provider, the actual passage of time. A *test double* is
a stand-in for such a dependency. Two kinds appear in this chapter: a
*fake* is a genuinely working but lightweight implementation, like an
in-memory map pretending to be a database; a *stub* is even simpler: it
returns canned answers, often from a single method, such as "every query
fails with this error". In many languages test doubles require mocking
frameworks and code generation. In Go they are ordinary structs, because
of how interfaces work: an interface names a set of methods, and any type
with those methods satisfies it implicitly. If your handler depends on a
`Repository` interface rather than a concrete database type, then any
five-line struct with the same methods can step in, and the handler
cannot tell the difference. That is the whole trick, and this chapter
uses it constantly.

Finally, *in-memory HTTP testing*. The naive way to test a web server is
to actually start it: listen on a port, send requests over the network,
wait for responses. Every item in that sentence is a liability: ports
collide when tests run in parallel, network buffers add timing you do not
control, and waits turn into `time.Sleep` calls that make the suite slow
and *flaky* (failing occasionally without any code change, the most
corrosive property a test can have). The in-memory approach removes the
transport entirely: the test hands a request object directly to the
router, in the same process, through the same code path a real request
would take once past the socket. No port, no network, no waiting. When
the call returns, the handler has run. Tests become deterministic and
fast enough to run on every save, which is what makes them actually get
run.

## How Testing Works in Iris

`httptest.New(t, app)` prepares an application for testing and returns an
`*httpexpect.Expect` value, a fluent client from the httpexpect library
that Iris integrates:

```go
import "github.com/kataras/iris/v14/httptest"

e := httptest.New(t, app)
```

Three things happen inside that call, and each one matters:

1. **The application is built.** `httptest.New` calls `app.Build()` for
   you and reports a build failure through `t.Fatalf`: a misconfigured
   route or an unresolvable dependency fails the test immediately with
   the real error message, instead of panicking.
2. **Requests bypass the network.** The returned client's transport is
   bound directly to the application. `e.GET("/x")` invokes the router
   in-process; nothing listens on a port, so tests are fast, parallel-safe
   and free of port conflicts.
3. **Logs are silenced.** The application's log level is set to
   `disable` by default, so `go test` output stays readable. Pass
   `httptest.LogLevel("debug")` or `httptest.Debug(true)` when you need
   to see what a failing test is doing.

\[Diagram: httptest.New binds the client transport straight to your built application so requests never touch the network, NewServer puts a real port in front of it, and Do calls a single handler with no router at all.]

The first argument is not literally a `*testing.T`: it is the
`httptest.IrisTesty` interface, which `*testing.T`, `*testing.B` and
Ginkgo's `GinkgoT()` all satisfy, so the same helper serves tests,
benchmarks and BDD suites.

The `httptest` package also re-exports every HTTP status code
(`httptest.StatusOK`, `httptest.StatusNotFound`, ...), so test files do
not need to import `net/http` just for constants.

## Your First Test

The smallest possible round trip:

```go
package main

import (
    "testing"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/httptest"
)

func TestHello(t *testing.T) {
    app := iris.New()
    app.Get("/hello", func(ctx iris.Context) {
        ctx.JSON(iris.Map{"message": "hello"})
    })

    e := httptest.New(t, app)
    e.GET("/hello").Expect().
        Status(httptest.StatusOK).
        JSON().Object().
        HasValue("message", "hello")
}
```

Every request follows the same shape: build the request (`e.GET(...)`,
optionally with body, query and headers), execute it with `Expect()`,
then chain assertions on the response. A failed assertion reports through
the `t` you passed to `httptest.New`, with the expected and actual values.

## Designing for Testability

The single most important testing decision is not in a test file at all.
It is the signature of your application factory:

```go
func newApp(cfg Config, repo store.Repository) *iris.Application
```

`main` calls it with production values; tests call it with their own.
Everything else follows from this:

- **Seams are parameters.** Anything a test may want to replace, such as
  the repository, a mail sender or a clock, arrives as an argument
  (usually an interface) and is handed to the builder's `Services` step,
  which the
  dependency container uses to satisfy controller constructors
  ([Chapter 8](https://iris-go.com/docs/dependency-injection/)).
- **Tests exercise the real application.** Because `newApp` is the same
  function `main` uses, every test request passes through the real
  router, the real middleware chain and the real central error map. There
  is no "test router" that behaves differently from production.
- **Configuration is a value.** A `Config` struct built in the test
  replaces files and environment variables; nothing global to reset
  between tests.

The Bookshelf service in the framework's `TUTORIAL.md` is a complete,
compile-verified worked example of this pattern. This chapter uses a
smaller application in the same shape.

## The Application Under Test

A notes service: a domain package that owns the entities, the validation
and the storage interface, and a `main` package that wires controllers
behind `iris.NewBuilder` ([Chapter 3](https://iris-go.com/docs/iris-newbuilder/)).

```go
// store/store.go
package store

import (
    "context"
    "errors"
    "sync"
    "uuid"

    "github.com/kataras/iris/v14/rest/errors/validation"
)

// ErrNotFound is a domain sentinel; the HTTP layer maps it to 404 once.
var ErrNotFound = errors.New("store: note not found")

type Note struct {
    ID   uuid.UUID `json:"id"`
    Text string    `json:"text"`
}

// NoteInput is the request payload. Implementing Validate makes
// ctx.BindJSON run it automatically after decoding (Chapter 5).
type NoteInput struct {
    Text string `json:"text"`
}

func (in NoteInput) Validate() error {
    return validation.Join(
        validation.String("text", in.Text).NotEmpty().Length(1, 280),
    )
}

// Repository is the storage seam: production wires a database
// implementation, tests wire MemStore or a one-method stub.
type Repository interface {
    Insert(ctx context.Context, n Note) error
    Notes(ctx context.Context) ([]Note, error)
    Note(ctx context.Context, id uuid.UUID) (Note, error) // ErrNotFound
}

// MemStore is the in-memory Repository: this chapter's storage and the
// test suite's default fake.
type MemStore struct {
    mu    sync.RWMutex
    notes map[uuid.UUID]Note
}

var _ Repository = (*MemStore)(nil)

func NewMemStore() *MemStore {
    return &MemStore{notes: make(map[uuid.UUID]Note)}
}

func (s *MemStore) Insert(_ context.Context, n Note) error {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.notes[n.ID] = n
    return nil
}

func (s *MemStore) Notes(_ context.Context) ([]Note, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    list := make([]Note, 0, len(s.notes))
    for _, n := range s.notes {
        list = append(list, n)
    }
    return list, nil
}

func (s *MemStore) Note(_ context.Context, id uuid.UUID) (Note, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    n, ok := s.notes[id]
    if !ok {
        return Note{}, ErrNotFound
    }
    return n, nil
}
```

The HTTP layer: two controllers and the composition root.

```go
// main.go
package main

import (
    "os"
    "uuid"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/middleware/basicauth"
    "github.com/kataras/iris/v14/rest/errors"

    "notes/store"
)

// NotesAPI serves /api/notes. Its constructor declares its dependency;
// the container calls it with the Repository registered in Services.
type NotesAPI struct {
    repo store.Repository
}

func NewNotesAPI(repo store.Repository) *NotesAPI {
    return &NotesAPI{repo: repo}
}

func (api *NotesAPI) Routing(r iris.Router) {
    r.Get("/", api.list)
    r.Post("/", api.create)
    r.Get("/{id:uuid}", api.get)
}

func (api *NotesAPI) list(ctx iris.Context) {
    notes, err := api.repo.Notes(ctx)
    ctx.List(notes, err)
}

func (api *NotesAPI) create(ctx iris.Context) {
    input, ok := ctx.BindJSON[store.NoteInput]()
    if !ok {
        return // 400 already written: malformed JSON or failed Validate.
    }

    n := store.Note{ID: uuid.New(), Text: input.Text}
    ctx.Created(n.ID, api.repo.Insert(ctx, n))
}

func (api *NotesAPI) get(ctx iris.Context) {
    id := ctx.Params().GetUUID("id")

    n, err := api.repo.Note(ctx, id)
    ctx.OK(n, err) // store.ErrNotFound renders 404 (central map).
}

// AdminAPI is mounted behind basic auth in newApp.
type AdminAPI struct {
    repo store.Repository
}

func NewAdminAPI(repo store.Repository) *AdminAPI {
    return &AdminAPI{repo: repo}
}

func (api *AdminAPI) Routing(r iris.Router) {
    r.Get("/stats", api.stats)
}

func (api *AdminAPI) stats(ctx iris.Context) {
    notes, err := api.repo.Notes(ctx)
    ctx.OK(iris.Map{"notes": len(notes)}, err)
}

// Config carries what tests want to control. A real application loads
// it from YAML and the environment (Chapter 11).
type Config struct {
    AdminUser     string
    AdminPassword string
}

// newApp is the composition root and the test seam: main passes the
// real configuration and store, tests pass their own.
func newApp(cfg Config, repo store.Repository) *iris.Application {
    return iris.NewBuilder().
        Prefix("/api").
        AllowOrigin("none").
        Compression(false).
        LogRequests(false).
        NoHealth().
        Errors(errors.NewOptions().
            MapErrors(errors.NotFound, store.ErrNotFound)).
        Services(repo).
        RouterMiddlewares().
        Middlewares().
        API("/notes", NewNotesAPI).
        API("/admin", basicauth.Default(map[string]string{
            cfg.AdminUser: cfg.AdminPassword,
        }), NewAdminAPI).
        Build()
}

func main() {
    cfg := Config{
        AdminUser:     "admin",
        AdminPassword: os.Getenv("ADMIN_PASSWORD"),
    }

    app := newApp(cfg, store.NewMemStore())
    app.Listen(":8080")
}
```

Note what `newApp` does not do: it does not read files, does not touch
`os.Getenv`, does not construct its own storage. Those are `main`'s job.
That separation is the entire trick.

## End-to-End Tests

The first test walks the happy path and the two most common failure
paths in one function. Each test builds its own application and store,
so tests share no state and can run in parallel.

```go
// main_test.go
package main

import (
    "testing"
    "uuid"

    "github.com/kataras/iris/v14/httptest"

    "notes/store"
)

func testConfig() Config {
    return Config{AdminUser: "admin", AdminPassword: "test-password"}
}

func TestNotesCRUD(t *testing.T) {
    app := newApp(testConfig(), store.NewMemStore())
    e := httptest.New(t, app)

    // Create; extract the generated id from the response.
    id := e.POST("/api/notes").
        WithJSON(store.NoteInput{Text: "buy milk"}).
        Expect().Status(httptest.StatusCreated).
        JSON().Object().Value("id").String().NotEmpty().Raw()

    // Read back.
    e.GET("/api/notes/"+id).Expect().Status(httptest.StatusOK).
        JSON().Object().HasValue("text", "buy milk")

    // The listing contains exactly one note.
    e.GET("/api/notes").Expect().Status(httptest.StatusOK).
        JSON().Array().Length().IsEqual(1)

    // Validation: ctx.BindJSON runs NoteInput.Validate -> 400.
    e.POST("/api/notes").
        WithJSON(store.NoteInput{Text: ""}).
        Expect().Status(httptest.StatusBadRequest)

    // The central error map turns store.ErrNotFound into 404.
    e.GET("/api/notes/" + uuid.New().String()).
        Expect().Status(httptest.StatusNotFound)
}
```

Notice `.Raw()`: most chain methods keep asserting, but `Raw()` steps out
of the chain and hands you the underlying Go value: here the created id
as a `string`, immediately reused to build the next request.

## The Request and Assertion Chain

Requests are built by chaining, executed by `Expect()`, and verified by
chaining again. The methods you will use constantly:

Building the request:

| Method                                       | Purpose                                           |
| -------------------------------------------- | ------------------------------------------------- |
| `e.GET/POST/PUT/PATCH/DELETE(path)`          | start a request                                   |
| `e.GET("/users/{id}", "42")`                 | fill path placeholders positionally               |
| `WithJSON(v)`                                | JSON-encode `v` as the body, set the content type |
| `WithQuery("page", 2)`                       | add a query parameter                             |
| `WithHeader("Authorization", "Bearer "+tok)` | add a header                                      |
| `WithBasicAuth(user, pass)`                  | add an `Authorization: Basic` header              |
| `WithFormField(k, v)`                        | add a form field                                  |
| `WithMultipart().WithFile(field, name)`      | multipart file upload                             |

Asserting on the response:

| Method                                               | Purpose                              |
| ---------------------------------------------------- | ------------------------------------ |
| `Status(httptest.StatusOK)`                          | status code                          |
| `Body().IsEqual("ok")`, `Body().Contains("<h1>")`    | raw body                             |
| `Header("X-Request-Id").NotEmpty()`                  | response header                      |
| `JSON().Object()`                                    | parse the body as a JSON object      |
| `JSON().Array()`                                     | parse the body as a JSON array       |
| `HasValue("name", "John")`                           | object has key with exact value      |
| `ContainsKey("id")` / `NotContainsKey("password")`   | key presence                         |
| `NotContainsValue(v)`                                | no key holds this value              |
| `Value("address").Object()`                          | descend into a nested value          |
| `Array().Length().IsEqual(3)` / `Array().NotEmpty()` | array size                           |
| `Array().Value(0).Object()`                          | descend into an element              |
| `Raw()`                                              | leave the chain, return the Go value |

Typed value assertions hang off `Value`:

```go
obj := e.GET("/api/report").Expect().
    Status(httptest.StatusOK).JSON().Object()

obj.Value("count").Number().IsEqual(42)
obj.Value("active").Boolean().IsTrue()
obj.Value("tags").Array().NotEmpty()
```

By default all failed assertions in a test are collected and reported
together. Pass `httptest.Strict(true)` to stop at the first failure
instead, useful when later assertions only produce noise once the first
one fails.

## Fakes and One-Method Stubs

Because the repository is an interface, a test double is ordinary Go.
The full in-memory `MemStore` covers most tests, but the sharpest tool
is the one-method stub: embed the interface, override a single method,
inherit everything else.

```go
import (
    stdContext "context"
    "fmt"
)

// failingRepo overrides one method of a working Repository; everything
// else is inherited. Interface embedding makes one-method fakes
// one-liners.
type failingRepo struct {
    store.Repository
}

func (failingRepo) Notes(stdContext.Context) ([]store.Note, error) {
    return nil, fmt.Errorf("pg: connection refused on 10.0.0.3:5432")
}

// Internal error details must never reach the client.
func TestInternalErrorsDoNotLeak(t *testing.T) {
    app := newApp(testConfig(), failingRepo{store.NewMemStore()})
    e := httptest.New(t, app)

    body := e.GET("/api/notes").
        Expect().Status(httptest.StatusInternalServerError).
        JSON().Object()
    body.HasValue("message", "server error")
    body.NotContainsValue("10.0.0.3") // stays server-side.
}
```

This is the test to internalize. Five lines of stub prove a property of
the whole system: an unmapped error (a database outage, a bug) renders
as a generic 500 while the real cause (with its connection string) is
logged server-side, never serialized to the client
([Chapter 9](https://iris-go.com/docs/error-handling/)). No mocking framework, no code
generation: the seam is an interface, so the stub is a struct literal.

## Testing Guards and Authentication

Protected surfaces get three tests: no credentials, wrong credentials,
valid credentials. With the in-memory client this is three requests:

```go
func TestAdminRequiresCredentials(t *testing.T) {
    app := newApp(testConfig(), store.NewMemStore())
    e := httptest.New(t, app)

    // No credentials: the basicauth middleware replies 401.
    e.GET("/api/admin/stats").Expect().
        Status(httptest.StatusUnauthorized)

    // Wrong credentials: still 401.
    e.GET("/api/admin/stats").WithBasicAuth("admin", "wrong").
        Expect().Status(httptest.StatusUnauthorized)

    // Valid credentials: through to the handler.
    e.GET("/api/admin/stats").WithBasicAuth("admin", "test-password").
        Expect().Status(httptest.StatusOK).
        JSON().Object().HasValue("notes", 0)
}
```

The password is not a secret here: `testConfig()` chose it. That is the
`newApp(cfg, ...)` seam again: authentication is tested against the real
middleware with credentials the test controls.

Token-protected routes follow the same pattern with a header instead:

```go
e.GET("/api/profile").
    WithHeader("Authorization", "Bearer "+token).
    Expect().Status(httptest.StatusOK)
```

Generate `token` with the same signer configuration the application
verifies with, a test-only key pair in the config, exactly like the
test-only password ([Chapter 10](https://iris-go.com/docs/authentication-authorization/)).

For guards you wrote yourself (the "shelf must exist", "shelf must be
unlocked" middlewares of [Chapter 4](https://iris-go.com/docs/routing-controllers/)), assert
the pair of behaviors that defines a guard: the request it blocks and
the request it lets through. A locked-resource guard, for example, gets
one test proving writes return 403 and one proving reads still return
200\.

## Testing Validation and the Error Map

Do not stop at status codes: your API's error body is a contract too
([Chapter 9](https://iris-go.com/docs/error-handling/)). Clients parse `http_error_code` and
`validation`, so pin the wire shape in a test.

```go
func TestValidationWireFormat(t *testing.T) {
    app := newApp(testConfig(), store.NewMemStore())
    e := httptest.New(t, app)

    body := e.POST("/api/notes").
        WithJSON(store.NoteInput{Text: ""}).
        Expect().Status(httptest.StatusBadRequest).
        JSON().Object()

    body.Value("http_error_code").Object().
        HasValue("canonical_name", "INVALID_ARGUMENT").
        HasValue("status", 400)
    body.Value("validation").Array().NotEmpty()
    body.Value("validation").Array().Value(0).Object().
        HasValue("field", "text")
}
```

The central error map deserves its own test per mapped sentinel: return
the sentinel from the seam (or trigger it naturally, as here with an
unknown id) and assert the canonical rendering:

```go
func TestNotFoundMapping(t *testing.T) {
    app := newApp(testConfig(), store.NewMemStore())
    e := httptest.New(t, app)

    e.GET("/api/notes/" + uuid.New().String()).
        Expect().Status(httptest.StatusNotFound).
        JSON().Object().
        Value("http_error_code").Object().
        HasValue("canonical_name", "NOT_FOUND").
        HasValue("status", 404)
}
```

These tests are cheap and catch a whole class of regressions: someone
removing a `MapErrors` line, a sentinel silently replaced by a new error
value, a handler that starts writing its own ad-hoc 404 body.

## Testing Middleware in Isolation

To test one middleware, build a throwaway application around it: the
middleware, one trivial handler, nothing else. The test then observes
the middleware's effect from the outside, like a client would.

```go
package middleware_test

import (
    "testing"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/httptest"
)

func TestVersionHeader(t *testing.T) {
    app := iris.New()
    app.Use(func(ctx iris.Context) {
        ctx.SetHeader("X-API-Version", "v1")
        ctx.Next()
    })
    app.Get("/", func(ctx iris.Context) {
        ctx.Text("ok")
    })

    e := httptest.New(t, app)
    e.GET("/").Expect().Status(httptest.StatusOK).
        Header("X-API-Version").IsEqual("v1")

    // Root-level Use runs for every request, even unmatched ones —
    // the header appears on the 404 too (Chapter 7).
    e.GET("/missing").Expect().Status(httptest.StatusNotFound).
        Header("X-API-Version").IsEqual("v1")
}
```

A middleware that makes decisions gets the same blocked/allowed pair as
any guard. Test the middleware you actually ship: the key check below is
`apikey.New` from [Chapter 12](https://iris-go.com/docs/built-in-middleware/#api-keys), not a
hand-written one, so the test covers the code that will run in
production:

```go
import (
    "testing"

    "github.com/kataras/iris/v14"
    "github.com/kataras/iris/v14/httptest"
    "github.com/kataras/iris/v14/middleware/apikey"
)

func TestAPIKeyGuard(t *testing.T) {
    app := iris.New()
    app.Use(apikey.New(apikey.Options{
        Header: "X-API-Key",
        Scheme: apikey.NoScheme, // a bare value, no "Bearer" prefix.
        Keys:   map[string]string{"reporting": "secret"},
    }))
    app.Get("/", func(ctx iris.Context) {
        principal, _ := apikey.Get(ctx)
        ctx.Text(principal)
    })

    e := httptest.New(t, app)

    // Blocked: no key, and a key that matches nothing.
    e.GET("/").Expect().Status(httptest.StatusUnauthorized)
    e.GET("/").WithHeader("X-API-Key", "nope").
        Expect().Status(httptest.StatusUnauthorized)

    // Allowed: the handler runs and can name the caller.
    e.GET("/").WithHeader("X-API-Key", "secret").
        Expect().Status(httptest.StatusOK).
        Body().IsEqual("reporting")
}
```

The third assertion is the one worth writing. A guard test that only
checks 401 and 200 proves the door opens; asserting the body is
`"reporting"` proves the request arrived carrying the caller's identity,
which is what every downstream audit line and rate-limit bucket depends
on.

Both tests are deterministic by construction: nothing waits, nothing
depends on timing. Prefer designs like this over `time.Sleep`: a test
that waits for wall-clock time is slow on every run and flaky on busy
CI machines.

## Unit Testing Domain Logic

Not everything needs an HTTP request. Domain rules (validation,
calculations, state transitions) live in packages that import nothing
from Iris, so they get plain Go tests, usually table-driven.
"Table-driven" is the standard Go idiom for testing many cases without
repeating the machinery: the cases are rows of data (a name, an input,
an expected outcome) and one loop runs the same check over every row.
Adding the next edge case is adding a line to the table, and `t.Run`
gives each row its own name in the failure output:

```go
// store/store_test.go
package store

import (
    "strings"
    "testing"
)

func TestNoteInputValidate(t *testing.T) {
    tests := []struct {
        name    string
        input   NoteInput
        wantErr bool
    }{
        {"valid", NoteInput{Text: "buy milk"}, false},
        {"empty", NoteInput{Text: ""}, true},
        {"too long", NoteInput{Text: strings.Repeat("x", 281)}, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.input.Validate()
            if (err != nil) != tt.wantErr {
                t.Fatalf("Validate() error = %v, wantErr %v",
                    err, tt.wantErr)
            }
        })
    }
}
```

Keep the split intentional: unit tests pin the domain's edge cases
cheaply and exhaustively; the HTTP tests of the previous sections pin
the wiring: that a failed `Validate` becomes a 400 with the right body.
One wire-format test per input type is enough; the case matrix belongs
here.

## httptest Options and Variants

`httptest.New` accepts options after the application:

```go
e := httptest.New(t, app,
    httptest.Debug(true),           // print requests and responses
    httptest.LogLevel("debug"),     // re-enable application logs
    httptest.Strict(true),          // stop at the first failed assertion
    httptest.URL("http://example.com"), // base URL for the client
)
```

| Option             | Default     | Effect                                          |
| ------------------ | ----------- | ----------------------------------------------- |
| `Debug(bool)`      | `false`     | dumps each request/response through `t.Logf`    |
| `LogLevel(string)` | `"disable"` | the application's logger level while testing    |
| `Strict(bool)`     | `false`     | require-style reporting: fail fast              |
| `URL(string)`      | `""`        | base URL; enables cookie handling for that host |

The test client always carries a cookie jar, but the jar needs a host to
file cookies under. When a flow depends on cookies surviving across
requests (sessions, CSRF tokens), set a base URL
(`httptest.URL("http://example.com")` or the shorthand
`httptest.Domain`), and `Set-Cookie` responses are resent automatically
on subsequent requests within the same `e`.

Four variants cover the remaining cases:

**`httptest.NewController`** builds a throwaway application around one
controller, which is what a controller test wants and what `httptest.New`
cannot give it: `New` takes a built application, and the unit under test
is a `Routing` value.

```go
func TestNotesController(t *testing.T) {
    e := httptest.NewController(t, "/notes", &NotesAPI{Store: store.NewMemory()})

    e.GET("/notes").Expect().Status(httptest.StatusOK).
        JSON().Array().IsEmpty()
}
```

The variadic middleware is registered on the controller's own group, so
it runs for its routes and not for the 404 and 405 chains:
`httptest.NewController(t, "/notes", api, requireAdmin)`. A failure
answers what a builder application answers, so the test asserts the body
production sends. The path parameter type and context error handlers are
installed, so a bad path parameter renders the `rest/errors` body.
`errors.Handler` runs before the router, so a bare failure status, such
as a `requireAdmin` that stops with 403, answers the JSON envelope and
not an empty body. It is also the 404 handler, so an unknown path under
the prefix answers the `NOT_FOUND` envelope. A method the controller
does not serve answers that same 404, not a 405, because
`Options.Router.FireMethodNotAllowed` is off by default and
`NewController` takes no options. Nothing else the builder chain adds is
there: no CORS, no access log, no recovery, no compression. When a test
needs those, or needs `httptest.URL`, it builds the application itself
and calls `New`.

**`httptest.Do`** executes a single handler with no application and no
routing, using the standard recorder (the package aliases
`httptest.NewRecorder` and `httptest.NewRequest` for convenience):

```go
func TestHandlerAlone(t *testing.T) {
    handler := func(ctx iris.Context) {
        ctx.JSON(iris.Map{"status": "ok"})
    }

    w := httptest.NewRecorder()
    r := httptest.NewRequest("GET", "/", nil)
    httptest.Do(w, r, handler)

    if w.Code != httptest.StatusOK {
        t.Fatalf("expected 200, got %d", w.Code)
    }
}
```

Because no routing happens, dynamic path parameters are never populated:
`ctx.Params()` is empty. A handler that reads path parameters must be
tested through a real application with `httptest.New`.

**`httptest.NewServer(t, app)`** builds the application and wraps it in
a standard `net/http/httptest.Server`, a real listener on a random
local port. Reach for it only when something genuinely needs a network
endpoint, such as testing an HTTP client:

```go
srv := httptest.NewServer(t, app)
defer srv.Close()

resp, err := http.Get(srv.URL + "/api/notes")
```

**`httptest.NewInsecure(t, ...)`** returns a client that skips TLS
certificate verification, for asserting against an already-running
HTTPS server: `httptest.NewInsecure(t, httptest.URL("https://localhost:8443"))`.

## Running the Suite

Nothing about Iris tests is special to the `go` tool:

```sh
go test ./...           # the whole module
go test -run CRUD ./... # a subset by test name
go test -race ./...     # with the race detector
go test -cover ./...    # with coverage
```

A word on `-race`: a *data race* is two goroutines (Go's lightweight
concurrent threads) touching the same memory at the same time with at
least one writing, and it corrupts data in ways that surface far from
the cause, often only under production load. Go ships a *race detector*
that watches memory accesses while tests run and reports races with full
stack traces the moment they happen.

Run `-race` in CI at minimum. Handlers execute concurrently in
production, and shared fakes like `MemStore` are exactly where an
unguarded map write hides; the race detector finds it while the suite is
still milliseconds long.

Determinism is a design property, and the patterns of this chapter buy
it for free, so keep them:

- **Fresh state per test.** Every test calls `newApp` with its own
  store. No shared globals means no ordering dependencies, and
  `t.Parallel()` is safe.
- **No ports.** `httptest.New` needs none; reserve `NewServer` for the
  rare test that truly requires a listener.
- **No sleeps.** The in-memory transport is synchronous: when
  `Expect()` returns, the handler has finished. If a test wants time to
  pass, inject a clock through the same seam as every other dependency
  instead of waiting for the real one.
- **In-memory stores by default.** Real databases belong to a separate,
  explicitly tagged integration suite
  ([Chapter 13](https://iris-go.com/docs/database-integration/)); the everyday suite must
  run anywhere `go test` does.

## Summary

- `httptest.New(t, app)` builds the application (failures reach
  `t.Fatalf`), binds an httpexpect client directly to it (no ports, no
  network) and silences logs. Options: `Debug`, `LogLevel`, `Strict`,
  `URL`.
- Requests chain builders (`WithJSON`, `WithQuery`, `WithHeader`,
  `WithBasicAuth`), execute with `Expect()`, and assert with `Status`,
  `JSON().Object()/Array()`, `HasValue`, `Value`, `Length`, `Body`,
  `Header`; `Raw()` returns the underlying Go value.
- Testability is an architecture decision: `newApp(cfg, deps)` takes its
  seams as parameters, so tests run the real router, middleware and
  error map with fakes only at the edges.
- Interface embedding turns one-method stubs into one-liners; the
  `failingRepo` test proves internal error details never leak to
  clients.
- Test guards as pairs (blocked and allowed), pin the validation and
  error-map wire format, and test middleware by building a tiny
  application around it.
- Domain logic gets plain table-driven Go tests; `go test ./...` runs
  everything in milliseconds, `-race` in CI, no sleeps anywhere.

## Further Reading

- [Go tutorial: Add a test](https://go.dev/doc/tutorial/add-a-test):
  the official first-test walkthrough; start here if `go test` itself is
  new.
- [The Go testing package](https://pkg.go.dev/testing): `*testing.T`,
  subtests with `t.Run`, `t.Parallel`, benchmarks and fuzzing.
- [The Go net/http/httptest package](https://pkg.go.dev/net/http/httptest):
  the standard library's recorder and test server that Iris's
  `httptest` builds upon.
- [httpexpect](https://pkg.go.dev/github.com/gavv/httpexpect/v2): the
  full assertion-chain API behind the client `httptest.New` returns.
- [The Go Data Race Detector](https://go.dev/doc/articles/race_detector):
  what `-race` catches, its cost, and how to read its reports.
