# Test the App

> Test the blog App's routes with tinkatest and its posts:list Command with clitest, swap the post store for a test, and run everything with act test.

This chapter writes tests for everything the App does so far. You will learn how to:

- Send requests to the App in a test with `tinkatest`
- Assert on statuses, JSON bodies, problem details and invalid fields
- Run a Command in a test with `clitest`
- Give a test its own version of a service with `tinkatest.Fake`
- Run the tests with `act test` and `go test`

## Overview

A Tinka App's tests are Go tests: `TestXxx` functions in `_test.go` files, run by `go test`. The skeleton adds two helpers. `tests.App(t)` builds the App the way `main` does, in the `test` Mode, with the values in `.env.test`. `tinkatest.New` starts that App and returns a client whose requests go straight to the App's handler, without a network, so each one takes microseconds.

Each test builds its own App, and each App has its own container, so each test gets its own store. A post one test creates never shows up in another, and every test can run in parallel with `t.Parallel()`.

## Test the routes

`act make:test` creates an HTTP test in `tests/functional`:

```sh
act make:test posts
```

```text title="Output"
CREATE tests/functional/posts_test.go
```

The generated test requests `GET /posts` and checks the status. Replace it with tests for each route. `Send(t)` sends a request and ties the response to the test, so a failed assertion reports the line that sent it, and each assertion returns the response, so they chain:

- `AssertStatus` checks the status code.
- `AssertJSONPath` checks one value in a JSON body, by keys and indexes joined with dots: `0.title` is the first post's title.
- `AssertJSON` checks the whole body against a value, here a `posts.Post`, which it compares as JSON.
- `AssertProblem` checks a problem details error by its status and its `code`.
- `AssertInvalid` checks for a `422` that names each of the fields.

Subtests are named as sentences that say what the route does, so a failure reads as the behavior that broke:

```go title="tests/functional/posts_test.go"
package functional_test

import (
	"net/http"
	"testing"

	"github.com/mucolabs/tinka/tinkatest"

	"blog/app/posts"
	"blog/tests"
)

func TestListPosts(t *testing.T) {
	t.Parallel()
	c := tinkatest.New(t, tests.App(t))
	c.Get("/posts").Send(t).
		AssertStatus(http.StatusOK).
		AssertJSONPath("0.title", "Hello, Tinka").
		AssertJSONPath("1.title", "Routes and handlers")
}

func TestShowPost(t *testing.T) {
	t.Parallel()

	t.Run("answers with the post", func(t *testing.T) {
		t.Parallel()
		c := tinkatest.New(t, tests.App(t))
		c.Get("/posts/1").Send(t).
			AssertStatus(http.StatusOK).
			AssertJSON(posts.Post{ID: 1, Title: "Hello, Tinka", Body: "The first post on the blog."})
	})

	t.Run("answers 404 for a post that doesn't exist", func(t *testing.T) {
		t.Parallel()
		c := tinkatest.New(t, tests.App(t))
		c.Get("/posts/99").Send(t).AssertProblem(http.StatusNotFound, "not_found")
	})
}

func TestCreatePost(t *testing.T) {
	t.Parallel()

	t.Run("answers 201 with the new post", func(t *testing.T) {
		t.Parallel()
		c := tinkatest.New(t, tests.App(t))
		c.Post("/posts").JSON(map[string]string{"title": "Testing", "body": "With tinkatest."}).Send(t).
			AssertStatus(http.StatusCreated).
			AssertJSONPath("id", 3)
		c.Get("/posts/3").Send(t).AssertJSONPath("title", "Testing")
	})

	t.Run("refuses a post without a title or a body", func(t *testing.T) {
		t.Parallel()
		c := tinkatest.New(t, tests.App(t))
		c.Post("/posts").JSON(map[string]string{"title": " "}).Send(t).
			AssertInvalid("title", "body")
	})
}
```

The create test sends a second request with the same client, which reaches the same App, so it proves the post was stored, not only returned.

## Test the Command

The `clitest` package runs Commands in a test, in the same process, the way `act` runs them. `app.Commands()` returns the App's Commands, and `Run` runs one with its arguments and returns its exit code, its stdout and its stderr.

To prove the Command lists what the container holds, the test gives the App a store of its own with a third post. `tinkatest.Fake` makes every `*posts.Store` the App resolves be the one you pass, until the test ends. Call it before the App starts: the Command starts the App when it runs. People's output ends with a newline after the result's `String`, so the expected text ends with one too.

Tests of one package go next to it, so create the file in `app/commands`. It's in the `commands_test` package, which can import `blog/tests` even though that package imports `app/commands` itself:

```go title="app/commands/posts_list_test.go"
package commands_test

import (
	"testing"

	"github.com/mucolabs/tinka/cli/clitest"
	"github.com/mucolabs/tinka/tinkatest"

	"blog/app/posts"
	"blog/tests"
)

func TestPostsList(t *testing.T) {
	t.Parallel()
	store := posts.NewStore()
	if _, err := store.Create(t.Context(), "Testing", "Commands too."); err != nil {
		t.Fatal(err)
	}
	app := tests.App(t)
	tinkatest.Fake(t, app, store)

	r := clitest.New(t, app.Commands()).Run("posts:list")
	want := "   1  Hello, Tinka\n   2  Routes and handlers\n   3  Testing\n"
	if r.Code != 0 || r.Stdout != want {
		t.Errorf("exit %d, stdout %q, stderr %q", r.Code, r.Stdout, r.Stderr)
	}
}
```

## Run the tests

`act test` runs every test of the App, and prints the `go test` command it runs first:

```sh
act test
```

```text title="Output"
$ GOWORK=off go test -json ./...
?   	blog	[no test files]
?   	blog/app/exceptions	[no test files]
?   	blog/app/handlers	[no test files]
?   	blog/app/posts	[no test files]
?   	blog/app/providers	[no test files]
?   	blog/config	[no test files]
?   	blog/start	[no test files]
?   	blog/tests	[no test files]
ok  	blog/app/commands	0.019s
ok  	blog/tests/functional	0.017s
9 tests passed in 10 packages
```

The nine tests are `TestHome`, which came with the skeleton, the three test functions and four subtests in `posts_test.go`, and `TestPostsList`. The tests are Go tests, so `go test` runs them the same way, in your editor and in CI:

```sh
go test ./...
```

```text title="Output"
?   	blog	[no test files]
ok  	blog/app/commands	0.008s
?   	blog/app/exceptions	[no test files]
?   	blog/app/handlers	[no test files]
?   	blog/app/posts	[no test files]
?   	blog/app/providers	[no test files]
?   	blog/config	[no test files]
?   	blog/start	[no test files]
?   	blog/tests	[no test files]
ok  	blog/tests/functional	0.009s
```

Flags after `act test` go to `go test`, so `act test -run TestCreatePost ./tests/functional` runs one test, and `act test --watch` runs the tests again on every change.

## Read a failure

A test's failure message should say what broke without opening the test. To see one, make the validation test also expect a `tags` field, which the App never reports:

```go title="tests/functional/posts_test.go" del={2} ins={3}
		c.Post("/posts").JSON(map[string]string{"title": " "}).Send(t).
			AssertInvalid("title", "body")
			AssertInvalid("title", "body", "tags")
```

Run the tests again:

```sh
act test
```

```text title="Output"
$ GOWORK=off go test -json ./...
?   	blog	[no test files]
ok  	blog/app/commands	(cached)
?   	blog/app/exceptions	[no test files]
?   	blog/app/handlers	[no test files]
?   	blog/app/posts	[no test files]
?   	blog/app/providers	[no test files]
?   	blog/config	[no test files]
?   	blog/start	[no test files]
?   	blog/tests	[no test files]
--- FAIL: TestCreatePost (0.00s)
    --- FAIL: TestCreatePost/refuses_a_post_without_a_title_or_a_body (0.00s)
        posts_test.go:56: POST /posts answered 422 Unprocessable Entity
            body: {"title":"Unprocessable Entity","status":422,"detail":"Some fields are invalid.","code":"invalid","fields":{"body":"Write the post's body.","title":"Enter a title."},"request_id":"WGZ6KP2QLCBOEGUFSJ5ODB74XL"}
            want errors for the fields title, body, tags, but there is none for tags
FAIL
FAIL	blog/tests/functional	0.011s
Error: 1 test failed.
  Fix: Run act test --failed to run only them again
```

The message names the request, the status and the body the App sent, then what the test wanted: the body has no `tags` field. `blog/app/commands` came from `go test`'s cache, since nothing it depends on changed. Remove `"tags"` again, and run only the test that failed:

```sh
act test --failed
```

```text title="Output"
$ GOWORK=off go test -json -run ^(TestCreatePost)$ blog/tests/functional
ok  	blog/tests/functional	0.009s
3 tests passed in 1 package
```

## Recap

`tests/functional/posts_test.go` tests each route through HTTP with `tinkatest`, and `app/commands/posts_list_test.go` runs `posts:list` with `clitest`, with a store of its own given by `tinkatest.Fake`. Each test builds its own App, so they all run in parallel. Next, [Deploy](deploy.md) builds the App for production.

See also: [Testing](../guides/testing/introduction.md), [HTTP tests](../guides/testing/http-tests.md), [Testing Commands](../guides/testing/commands.md), [Test doubles](../guides/testing/test-doubles.md)
