Integration test harness for CLIs using act.sh (run the subject) and assert.txt (ordered line expectations, with {{pattern}} placeholders).
go install github.com/ofthemachine/clitest/cmd/clitest@latestCreate clitest.yml in your project root (or pass -config):
binary_name: mytool
build_command: go build -o mytool .
project_root_marker: go.mod
test_dirs:
- "integration/**"
- "tests/cli/*"
patterns:
custom_id: "[a-z]{3}[0-9]+"
environment:
MY_ENV: test
copy_globs:
- extra-binary
recursive: truebuild_command: optional shell string (sh -c). Empty skips the build step.recursive: whenfalse, only each entry intest_dirsis checked foract.sh/assert.txt(no walk into subdirectories). Default istrue.test_dirs: globs relative to project root, or a trailing/**on one segment (e.g.cli_test/**→ scan that directory recursively).root: optional; if set, resolved relative to the config file’s directory and used as the project root instead of walking upward forproject_root_marker.
Run:
clitest # uses ./clitest.yml
clitest -config path.yml # explicit config
clitest -dir tests/one_case # single directory (overrides test_dirs)
clitest -parallel 4
clitest -v # print act output on success
clitest -versionExit code 1 if any case fails. Final line: SUMMARY pass=N fail=M.
Use from go test with the integration build tag (typical pattern):
//go:build integration
package main_test
import (
"path/filepath"
"runtime"
"testing"
"github.com/ofthemachine/clitest"
)
func TestCLI(t *testing.T) {
_, f, _, _ := runtime.Caller(0)
dir := filepath.Dir(f)
root := filepath.Clean(filepath.Join(dir, "..")) // adjust to your layout
clitest.RunSuite(t, clitest.Options{
RootDir: root,
BaseDirs: []string{dir},
EnvOverrideVar: "CLI_TEST_SUITE_DIR",
BinaryName: "mytool",
BuildCommand: []string{"go", "build", "-o", "mytool", "."},
ProjectRootMarker: "go.mod",
})
}RootDir: absolute or relative path to the project root (directory containingProjectRootMarker). Required when the test package is not inside the clitest module (e.g.filepath.Clean(filepath.Join(testDir, ".."))).NonRecursive: true: only eachBaseDiritself is checked foract.sh/assert.txt(no walk into subdirectories).
- Non-empty, non-
#lines are expectations (comments use#). - Matching mode is ordered lines with a lookahead window on the combined stdout+stderr.
- Placeholders:
{{hash8}},{{path}},{{any}},{{timestamp_ms}}, … seeBuiltinPatternsin code. - Custom regex inline:
{{name:yourRegexHere}}overrides the named pattern with a one-off regex fragment.
act.sh runs with TEST_TEMP_DIR set and the built binary (and copy_globs matches) on PATH ahead of the rest of the environment.
make build
make test
make test-integrationMIT