我需要嘲笑
exec.Command()
。
我可以使用以下方法来模拟它:
var rName string
var rArgs []string
mockExecCommand := func(name string, arg ...string) *exec.Cmd {
rName = name
rArgs = arg
return nil
}
但是,这在实际代码中不起作用,因为它会抱怨 nil 指针,因为返回的
exec.Cmd
会调用 Run()
。
我试着像这样嘲笑它:
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
}
但它抱怨:
cannot use &m (value of type *mock) as *exec.Cmd value in return statementcompilerIncompatibleAssign
。
有什么办法可以解决这个问题吗?有没有更好的方法来嘲笑
exec.Command()
?
如果我返回“模拟”命令,模拟函数就会起作用,尽管我也更喜欢控制
Run()
函数:
var rName string
var rArgs []string
mockExecCommand := func(name string, arg ...string) *exec.Cmd {
rName = name
rArgs = arg
return exec.Command("echo")
}
虽然劫持测试可执行文件来运行特定函数是有效的,但使用常规依赖注入会更直接。不需要魔法。
设计一个可以运行命令的界面(例如
CommandExecutor
),然后将其中之一作为运行命令所需的任何函数的输入。然后,您可以在测试期间提供满足接口的模拟实现(手工制作或使用您选择的工具(如 GoMock)生成)。为您的生产代码提供真正的实现(调用 exec
包)。您的模拟实现甚至可以对参数进行断言,以便您知道命令正在正确“执行”。
实际上有一种方法可以做到这一点。所有功劳都归功于this文章。请查看以下内容的解释:
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)
}
据我所知,Go 中最好的方法是使用多态性。你走在正确的轨道上。详细的解释位于 https://github.com/schollii/go-test-mock-exec-command,我创建它是因为当我搜索如何模拟
os/exec
时,我所能找到的只是 env 变量另一个答案中提到的技术。这种方法绝对没有必要,正如我在我链接的 git 存储库的自述文件中提到的,它所需要的只是一点多态性。
总结基本上是这样的:
exec.Cmd
创建一个接口类,该类仅包含应用程序(或模块)代码使用的必要方法exec.Cmd
它在应用程序代码中看起来像这样:
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
}
}
在测试方面,它看起来像这样:
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
}
为了扩展 Andrew M 和 Oliver 提到的内容,我喜欢这样做:
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)
}
测试代码:
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")
}
如何模拟 *exec.Cmd / exec.Command()?
你不能。提出一个非基于模拟的测试策略。