I'm building an UI for cli apps. I completed the functions but I couldn't figure out how to test it.
Repo: https://github.com/erdaltsksn/cui
func Success(message string) {
color.Success.Println("√", message)
os.Exit(0)
}
// Error prints a success message and exit status 1
func Error(message string, err ...error) {
color.Danger.Println("X", message)
if len(err) > 0 {
for _, e := range err {
fmt.Println(" ", e.Error())
}
}
os.Exit(1)
}
I want to write unit tests for functions. The problem is functions contains print
and os.Exit()
. I couldn't figure out how to write test for both.
This topic: How to test a function's output (stdout/stderr) in unit tests helps me test print function. I need to add os.Exit()
My Solution for now:
func captureOutput(f func()) string {
var buf bytes.Buffer
log.SetOutput(&buf)
f()
log.SetOutput(os.Stderr)
return buf.String()
}
func TestSuccess(t *testing.T) {
type args struct {
message string
}
tests := []struct {
name string
args args
output string
}{
{"Add test cases.", args{message: "my message"}, "ESC[1;32m"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
want := tt.output
got := captureOutput(func() {
cui.Success(tt.args.message)
})
got := err
if got.Error() != want {
t.Error("Got:", got, ",", "Want:", want)
}
})
}
}
The usual answer in TDD is that you take your function and divide it into two parts; one part that is easy to test, but is not tightly coupled to specific file handles, or a specific implementation of os::Exit; the other part is tightly coupled to these things, but is so simple it obviously has no deficiencies.
Your "unit tests" are mistake detectors that measure the first part.
The second part you write once, inspect it "by hand", and then leave it alone. The idea here being that things are so simple that, once implemented correctly, they don't need to change.
// Warning: untested code ahead
func Foo_is_very_stable() {
bar_is_easy_to_test(stdin, stdout, os.exit)
}
func bar_is_easy_to_test(in *File, out *File , exit func(int)) {
// Do complicated things here.
}
Now, we are cheating a little bit -- os.exit
is special magic that never returns, but bar_is_easy_to_test
doesn't really know that.
Another design that is a bit more fair is to put the complicated code into a state machine. The state machine decides what to do, and the host invoking the machine decides how to do that....
// More untested code
switch state_machine.next() {
case OUT:
println(state_machine.line())
state_machine.onOut()
case EXIT:
os.exit(state_machine.exitCode())
Again, you get a complicated piece that is easy to test (the state machine) and a much simpler piece that is stable and easily verified by inspection.
This is one of the core ideas underlying TDD - that we deliberately design our code in such a way that it is "easy to test". The justification for this is the claim that code that is easy to test is also easy to maintain (because mistakes are easily detected and because the designs themselves are "cleaner").
Recommended viewing