Testing Commands
This guide covers testing Commands. You will learn how to:
- Run a Command in a test and check its output
- Check its JSON result
- Answer its prompts
- Set environment variables for it
- Keep every Command's signature stable
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:
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
}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)
}
}Check the JSON result
Pass --json, and decode the result with Decode:
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:
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:
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:
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:
TINKA_UPDATE=1 go test -run TestSignatures .See also: Creating Commands, Prompts