Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock exec.Command for multiple unit tests in Go lang?

I just learnt unit testing functions that uses exec.Command() i.e., mocking exec.Command(). I went ahead to added more unit cases, but running into issues of not able to mock the output for different scenarios.

Here is a sample code hello.go I'm trying to test...

package main

import (
    "fmt"
    "os/exec"
)

var execCommand = exec.Command

func printDate() ([]byte, error) {
    cmd := execCommand("date")
    out, err := cmd.CombinedOutput()
    return out, err
}

func main() {
    fmt.Printf("hello, world\n")
    fmt.Println(printDate())
}

Below is the test code hello_test.go...

package main

import (
    "fmt"
    "os"
    "os/exec"
    "testing"
)

var mockedExitStatus = 1
var mockedDate = "Sun Aug 20"
var expDate = "Sun Aug 20"

func fakeExecCommand(command string, args ...string) *exec.Cmd {
    cs := []string{"-test.run=TestHelperProcess", "--", command}
    cs = append(cs, args...)
    cmd := exec.Command(os.Args[0], cs...)
    cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
    return cmd
}

func TestHelperProcess(t *testing.T) {
    if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
        return
    }

    // println("Mocked Data:", mockedDate)
    fmt.Fprintf(os.Stdout, mockedDate)
    os.Exit(mockedExitStatus)
}

func TestPrintDate(t *testing.T) {
    execCommand = fakeExecCommand
    defer func() { execCommand = exec.Command }()

    out, err := printDate()
    print("Std out: ", string(out))
    if err != nil {
        t.Errorf("Expected nil error, got %#v", err)
    }
    if string(out) != expDate {
        t.Errorf("Expected %q, got %q", expDate, string(out))
    }
}

func TestPrintDateUnableToRunError(t *testing.T) {
    execCommand = fakeExecCommand
    defer func() { execCommand = exec.Command }()

    mockedExitStatus = 1
    mockedDate = "Unable to run date command"
    expDate = "Unable to run date command"

    out, err := printDate()
    print("Std out: ", string(out))
    if err != nil {
        t.Errorf("Expected nil error, got %#v", err)
    }
    if string(out) != expDate {
        t.Errorf("Expected %q, got %q", expDate, string(out))
    }
}

go test fails for the second test TestPrintDateUnableToRunError...

$ go test hello
Std out: Sun Aug 20Std out: Sun Aug 20--- FAIL: TestPrintDateTomorrow (0.01s)
    hello_test.go:62: Expected "Unable to run date command", got "Sun Aug 20"
FAIL
FAIL    hello   0.017s

Even though I'm trying to set the global mockedDate value inside the test case, it's still getting the global value that it was initialized with. Is the global value not getting set? Or the changes to that global var is not getting updated in TestHelperProcess?

like image 515
abhijithda Avatar asked Aug 21 '17 03:08

abhijithda


People also ask

What is mocking in unit testing Golang?

Mocking is a way of creating a stub (a fake, a not the real, you name it) piece of code that substitutes some functions of the whole code. Its purpose is to make testing easier because you can control those stub functions' inputs and outputs freely which allows you to focus only on the business logic.


1 Answers

I got the solution for this...

Is the global value not getting set? Or the changes to that global var is not getting updated in TestHelperProcess?

Since in TestPrintDate(), fakeExecCommand is called instead of exec.Command, and calling fakeExecCommand runs go test to run only TestHelperProcess(), it's altogether a new invocation where only TestHelperProcess() will be executed. Since only TestHelperProcess() is called, the global variables aren't being set.

The solution would be to set the Env in the fakeExecCommand, and retrieve that in TestHelperProcess() and return those values.

PS> TestHelperProcess is renamed to TestExecCommandHelper, And few variables are renamed.

package main

import (
    "fmt"
    "os"
    "os/exec"
    "strconv"
    "testing"
)

var mockedExitStatus = 0
var mockedStdout string

func fakeExecCommand(command string, args ...string) *exec.Cmd {
    cs := []string{"-test.run=TestExecCommandHelper", "--", command}
    cs = append(cs, args...)
    cmd := exec.Command(os.Args[0], cs...)
    es := strconv.Itoa(mockedExitStatus)
    cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1",
        "STDOUT=" + mockedStdout,
        "EXIT_STATUS=" + es}
    return cmd
}

func TestExecCommandHelper(t *testing.T) {
    if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
        return
    }

    // println("Mocked stdout:", os.Getenv("STDOUT"))
    fmt.Fprintf(os.Stdout, os.Getenv("STDOUT"))
    i, _ := strconv.Atoi(os.Getenv("EXIT_STATUS"))
    os.Exit(i)
}

func TestPrintDate(t *testing.T) {
    mockedExitStatus = 1
    mockedStdout = "Sun Aug 201"
    execCommand = fakeExecCommand
    defer func() { execCommand = exec.Command }()
    expDate := "Sun Aug 20"

    out, _ := printDate()
    if string(out) != expDate {
        t.Errorf("Expected %q, got %q", expDate, string(out))
    }
}

func TestPrintDateUnableToRunError(t *testing.T) {
    mockedExitStatus = 1
    mockedStdout = "Unable to run date command"
    execCommand = fakeExecCommand
    defer func() { execCommand = exec.Command }()

    expDate := "Unable to run date command"

    out, _ := printDate()
    // println("Stdout: ", string(out))
    if string(out) != expDate {
        t.Errorf("Expected %q, got %q", expDate, string(out))
    }
}

go test results as below... (Purposely failing one test to show that the mock is working properly).

 go test hello
--- FAIL: TestPrintDate (0.01s)
        hello_test.go:45: Expected "Sun Aug 20", got "Sun Aug 201"
FAIL
FAIL    hello   0.018s
like image 95
abhijithda Avatar answered Sep 19 '22 14:09

abhijithda