Added unit tests for CLI
This commit is contained in:
1 parent
169c6c6b64
commit
5ec4b65b91
7 files changed
+965
No files matched your search
@@ -0,0 +1,151 @@
|
||||
// Package cmdtest runs job-search commands in tests, much as net/http/httptest
|
||||
// serves handlers. Commands read .env and config.yaml from the working
|
||||
// directory and print to os.Stdout, so Setup gives each test its own working
|
||||
// directory and Run swaps os.Stdin and os.Stdout for files.
|
||||
//
|
||||
// Tests using it change global state (the working directory, os.Stdin,
|
||||
// os.Stdout, Viper and the logging level), so they must not run in parallel.
|
||||
package cmdtest
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/cli"
|
||||
"git.avsdev.uk/Go/job-search/internal/logging"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Setup changes into a new temporary directory holding an empty .env and,
|
||||
// unless config is empty, config.yaml with those contents. It returns the
|
||||
// directory.
|
||||
func Setup(t *testing.T, config string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
WriteFile(t, ".env", "")
|
||||
if config != "" {
|
||||
WriteFile(t, "config.yaml", config)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
// WriteFile writes contents to path, failing the test on error.
|
||||
func WriteFile(t *testing.T, path, contents string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFile returns the contents of path, failing the test on error.
|
||||
func ReadFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Clean(path))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Exists reports whether path exists.
|
||||
func Exists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// NewRoot returns a root command set up like job-search's: the shared flags
|
||||
// bound to opts, logging set up before every command, opts.Finish run after,
|
||||
// and cmds added beneath it.
|
||||
func NewRoot(opts *cli.Options, cmds ...*cobra.Command) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "job-search",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli.SetupInitialLogging(cmd)
|
||||
return nil
|
||||
},
|
||||
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return opts.Finish()
|
||||
},
|
||||
}
|
||||
cobra.EnableTraverseRunHooks = true
|
||||
opts.Bind(root)
|
||||
root.AddCommand(cmds...)
|
||||
return root
|
||||
}
|
||||
|
||||
// Run runs args on a command tree from newRoot, with stdin as standard
|
||||
// input, and returns everything written to standard output (including log
|
||||
// messages). Viper and the logging level are reset around each run, as they
|
||||
// are global.
|
||||
func Run(
|
||||
t *testing.T, newRoot func() *cobra.Command, stdin string, args ...string,
|
||||
) (string, error) {
|
||||
t.Helper()
|
||||
viper.Reset()
|
||||
t.Cleanup(viper.Reset)
|
||||
|
||||
dir := t.TempDir()
|
||||
inPath, outPath := filepath.Join(dir, "stdin"), filepath.Join(dir, "stdout")
|
||||
WriteFile(t, inPath, stdin)
|
||||
in, err := os.Open(filepath.Clean(inPath))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer in.Close() //nolint:errcheck // read only
|
||||
out, err := os.Create(filepath.Clean(outPath))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldIn, oldOut, oldLog := os.Stdin, os.Stdout, slog.Default()
|
||||
os.Stdin, os.Stdout = in, out
|
||||
defer func() {
|
||||
os.Stdin, os.Stdout = oldIn, oldOut
|
||||
slog.SetDefault(oldLog)
|
||||
logging.SetLevel(slog.LevelInfo)
|
||||
}()
|
||||
|
||||
root := newRoot()
|
||||
root.SetArgs(args)
|
||||
runErr := root.Execute()
|
||||
|
||||
if err := out.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ReadFile(t, outPath), runErr
|
||||
}
|
||||
|
||||
// MustRun is Run with no input, for commands that must succeed.
|
||||
func MustRun(t *testing.T, newRoot func() *cobra.Command, args ...string) string {
|
||||
t.Helper()
|
||||
out, err := Run(t, newRoot, "", args...)
|
||||
if err != nil {
|
||||
t.Fatalf("job-search %s: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Contains fails the test unless out contains every string in want.
|
||||
func Contains(t *testing.T, out string, want ...string) {
|
||||
t.Helper()
|
||||
for _, w := range want {
|
||||
if !strings.Contains(out, w) {
|
||||
t.Errorf("output does not contain %q:\n%s", w, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ErrContains fails the test unless err is an error containing want.
|
||||
func ErrContains(t *testing.T, err error, want string) {
|
||||
t.Helper()
|
||||
if err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("err = %v; want an error containing %q", err, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user