Reputation: 995
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
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
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:
exec.Cmd
that has only the necessary methods to be used by your application (or module) codeexec.Cmd
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
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
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
Reputation: 42432
How to mock *exec.Cmd / exec.Command()?
You cannot. Come up with a non mock-based testing strategy.
Upvotes: -3