tinka alpha

Testing Commands

This guide covers testing Commands. You will learn how to:

Overview

The clitest package (github.com/mucolabs/tinka/cli/clitest) runs Commands in a test, in the same process, the way act runs them. A run sees no terminal and none of the process's environment, only what the test gives it, so a test behaves the same on your machine and in CI.

Run a Command

Pass the test and the root of the Commands to clitest.New, then Run the Command with its arguments and flags. app.Commands() returns the root of the App's Commands, Tinka's own included. Build the App in a function that main and the tests share:

main.go
package main

import (
	"os"

	"github.com/mucolabs/tinka"
)

func main() {
	os.Exit(newApp().Main(os.Args[1:]))
}

// newApp builds the App with its routes, providers and Commands.
func newApp() *tinka.App {
	app := tinka.New(tinka.HTTPConfig{})
	app.Command(greet, publishCommand(app))
	return app
}
commands_test.go
package main

import (
	"testing"

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

func TestGreet(t *testing.T) {
	r := clitest.New(t, newApp().Commands()).Run("greet", "Bea")
	if r.Code != 0 || r.Stdout != "Hello, Bea!\n" {
		t.Errorf("exit %d, stdout %q, stderr %q", r.Code, r.Stdout, r.Stderr)
	}
}
Codeint

The exit status: 0 on success, 1 on failure, 2 for a usage error.

Stdoutstring

The result, as people see it, or the JSON result with --json.

Stderrstring

Progress, status lines and errors.

Check the JSON result

Pass --json, and decode the result with Decode:

go
	r := clitest.New(t, newApp().Commands()).Run("posts:publish", "--limit", "5", "--json")
	var res struct {
		Result string `json:"result"`
		Count  int    `json:"count"`
	}
	if err := r.Decode(&res); err != nil || res.Result != "ok" {
		t.Fatalf("%v: %s", err, r.Stdout)
	}

Answer prompts

Without answers, a run has nobody to answer, as in CI: prompts return their flags' defaults, and required ones fail. That's often the simplest test, since passing the flags answers every prompt.

To test the prompts themselves, answer each one with Answer, by the name of its flag, or of the argument for a missing argument's prompt. Rewording a question then never breaks a test:

go
func TestMakeController(t *testing.T) {
	r := clitest.New(t, newApp().Commands()).
		Answer("name", "posts").
		Answer("title", "Hello").
		Answer("db", "postgres").
		Answer("resource", "y").
		Run("make:controller")
	if r.Code != 0 {
		t.Fatalf("exit %d: %s", r.Code, r.Stderr)
	}
}

Answer as a person would type: text, y or n for a confirmation, a choice's value for a selection, and an empty string for the default. A prompt the test didn't answer fails the run, and an answer no prompt used fails the test, so the test always matches the questions the Command asks. Terminal() runs with a person present and no answers, to check that a Command doesn't prompt at all.

Set environment variables

Env sets an environment variable for the runs, such as APP_ENV for a destructive Command:

go
	r := clitest.New(t, newApp().Commands()).Env("APP_ENV", "production").Run("db:wipe")
	if r.Code != 2 {
		t.Errorf("db:wipe must need --force in production, got exit %d", r.Code)
	}

Keep signatures stable

A Command's name, arguments and flags are an interface that scripts and agents depend on. clitest.Signatures checks every Command's signature, as --help --json describes it, against a file, so a change to one is always deliberate:

go
func TestSignatures(t *testing.T) {
	clitest.Signatures(t, newApp().Commands(), "testdata/commands.golden")
}

Run the test with TINKA_UPDATE=1 to write the file, and again after a change you meant, then commit it:

Terminal
TINKA_UPDATE=1 go test -run TestSignatures .

See also: Creating Commands, Prompts