doublethink13
doublethink13

Reputation: 995

How to mock *exec.Cmd / exec.Command()?

I need to mock exec.Command().

I can mock it using:

var rName string
var rArgs []string

mockExecCommand := func(name string, arg ...string) *exec.Cmd {
    rName = name
    rArgs = arg

    return nil
}

However, this won't work in the actual code, as it complains about the nil pointer, since the returning exec.Cmd calls Run().

I tried to mock it like:

type mock exec.Cmd

func (m *mock) Run() error {
    return nil
}

var rName string
var rArgs []string

mockExecCommand := func(name string, arg ...string) *exec.Cmd {
    rName = name
    rArgs = arg

    m := mock{}

    return &m
}

But it complains: cannot use &m (value of type *mock) as *exec.Cmd value in return statementcompilerIncompatibleAssign.

Is there any way to approach this? Is there a better way to mock exec.Command()?

The mocked function works if I return a "mock" command, although I'd prefer to control the Run() function too:

var rName string
var rArgs []string

mockExecCommand := func(name string, arg ...string) *exec.Cmd {
    rName = name
    rArgs = arg

    return exec.Command("echo")
}

Upvotes: 7

Views: 7244

Answers (5)

Nick
Nick

Reputation: 402

To expand on what Andrew M and Oliver have mentioned, here's what I like to do:

package main

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

// There's no need to export this interface
// Especially if the tests are in the same package
type commandExecutor interface {
    Output() ([]byte, error)
    // Other methods of the exec.Cmd struct could be added
}

// This var gets overwritten in your tests, as Oliver mentions
var shellCommandFunc = func(name string, arg ...string) commandExecutor {
    return exec.Command(name, arg...)
}

func GitVersion() (string, error) {
    // Make use of the wrapper function
    cmd := shellCommandFunc("git", "--version")
    out, err := cmd.Output()

    if err != nil {
        return "", err
    }

    return strings.TrimSpace(string(out))[12:], nil
}

func main() {
    version, _ := GitVersion()
    fmt.Println("Git Version:", version)
}

Test Code:

package main

import (
    "testing"

    "github.com/stretchr/testify/assert"
)

type MockCommandExecutor struct {
    // Used to stub the return of the Output method
    // Could add other properties depending on testing needs
    output string
}

// Implements the commandExecutor interface
func (m *MockCommandExecutor) Output() ([]byte, error) {
    return []byte(m.output), nil
}

func TestGitVersion(t *testing.T) {
    assert := assert.New(t)

    origShellCommandFunc := shellCommandFunc
    defer func() { shellCommandFunc = origShellCommandFunc }()

    shellCommandCalled := false
    shellCommandFunc = func(name string, args ...string) commandExecutor {
        shellCommandCalled = true

        // Careful: relies on implementation details this could
        // make the test fragile.
        assert.Equal("git", name, "command name")
        assert.Len(args, 1, "command args")
        assert.Equal("--version", args[0], "1st command arg")

        // Careful: if the stub deviates from how the system under
        // test works this could generate false positives.
        return &MockCommandExecutor{output: "git version 1.23.456\n"}
    }

    version, err := GitVersion()
    if assert.NoError(err) {
        assert.Equal("1.23.456", version, "version string")
    }

    // Ensure the test double is called
    assert.True(shellCommandCalled, "shell command called")
}

Upvotes: 2

Oliver
Oliver

Reputation: 29463

The best way that I know of in go is to use polymorphism. You were on the right track. A detailed explanation is at https://github.com/schollii/go-test-mock-exec-command, which I created because when I searched for how to mock os/exec, all I could find was the env variable technique mentioned in another answer. That approach is absolutely not necessary, and as I mention in the readme of the git repo I linked to, all it takes is a bit of polymorphism.

The summary is basically this:

  1. Create an interface class for exec.Cmd that has only the necessary methods to be used by your application (or module) code
  2. Create a struct that implements that interface, eg it can just mention exec.Cmd
  3. Create a package-level var (exported) that points to a function that returns the struct from step 2
  4. Make your application code use that package-level var
  5. Make your test create a new struct that implements that interface, but contains only outputs and exit codes, and make the test replace that package-level var by an instance of this new struct

It will look something like this in the application code:

type IShellCommand interface {
    Run() error
}

type execShellCommand struct {
    *exec.Cmd
}

func newExecShellCommander(name string, arg ...string) IShellCommand {
    execCmd := exec.Command(name, arg...)
    return execShellCommand{Cmd: execCmd}
}

// override this in tests to mock the git shell command
var shellCommander = newExecShellCommander

func myFuncThatUsesExecCmd() {
    cmd := shellCommander("git", "rev-parse", "--abbrev-ref", "HEAD")
    err := cmd.Run()
    if err != nil {
        // handle error
    } else {
        // process & handle output
    }
}

On the test side it will look something like this:

type myShellCommand struct {
    RunnerFunc func() error
}

func (sc myShellCommand) Run() error {
    return sc.RunnerFunc()
}

func Test_myFuncThatUsesExecCmd(t *testing.T) {
    // temporarily swap the shell commander
    curShellCommander := shellCommander
    defer func() { shellCommander = curShellCommander }()

    shellCommander = func(name string, arg ...string) IShellCommand {
        fmt.Printf("exec.Command() for %v called with %v and %v\n", t.Name(), name, arg)
        return myShellCommand{
            RunnerFunc: func() error {
                return nil
            },
        }
    }

    // now that shellCommander is mocked, call the function that we want to test:
    myFuncThatUsesExecCmd()
    // do checks
  }

Upvotes: 1

Andrew M.
Andrew M.

Reputation: 852

While hijacking the test executable to run a specific function works, it would be more straightforward to just use regular dependency injection. No magic required.

Design an interface (e.g. CommandExecutor) that can run commands, then take one of those as your input to whatever function needs to run a command. You can then provide a mock implementation that satisfies the interface (hand-crafted, or generated using your tool of choice, like GoMock) during your tests. Provide the real implementation (which calls into the exec package) for your production code. Your mock implementation can even make assertions on the arguments so that you know the command is being "executed" correctly.

Upvotes: 4

doublethink13
doublethink13

Reputation: 995

There is actually a way to do this. All credit goes to this article. Check it out for an explanation on what's going on below:

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
    }
    os.Exit(0)
}

Upvotes: 3

Volker
Volker

Reputation: 42432

How to mock *exec.Cmd / exec.Command()?

You cannot. Come up with a non mock-based testing strategy.

Upvotes: -3

Related Questions