0

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)
            }
        })
    }
}

erdaltsksn
  • 84
  • 8

2 Answers2

1

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

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
-1

What you have there is called a "side effect" - a situation when execution of your application spans beyond it's environment, it's address space. And the thing is, you don't test side effects. It is not always possible, and when it is - it is unreasonably complicated and ugly.

The basic idea is to have your side effects, like CLI output or os.Exit() (or network connections, or accessing files), decoupled from you main body of logic. There are plenty of ways to do it, the entire "software design" discipline is devoted to that, and @VoiceOfUnreason gives a couple of viable examples.

In your example I would go with wrapping side effects in functions and arranging some way to inject dependencies into Success() & Error(). If you want to keep those two just plain functions, then it's either a function argument or a global variable holding a function for exiting (as per @Peter's comment), but I'd recommend going OO way, employing some patterns and achieving much greater flexibility for you lib.

x1n13y84issmd42
  • 890
  • 9
  • 17