如何模拟 *exec.Cmd / exec.Command()?

huangapple go评论83阅读模式
英文:

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

问题

我需要模拟exec.Command()函数。

我可以使用以下方式进行模拟:

var rName string
var rArgs []string

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

	return 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()函数?

如果我返回一个"mock"命令,模拟函数是可以工作的,尽管我更希望能够控制Run()函数:

var rName string
var rArgs []string

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

	return exec.Command("echo")
}
英文:

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

</details>


# 答案1
**得分**: 3

虽然劫持测试可执行文件来运行特定函数是可行的但更简单的方法是使用常规的依赖注入不需要任何魔法

设计一个接口例如`CommandExecutor`),用于运行命令然后将其中一个命令作为输入传递给需要运行命令的函数在测试期间您可以提供一个满足该接口的模拟实现手工编写或使用您选择的工具如GoMock)。对于生产代码提供真正的实现调用`exec`)。您的模拟实现甚至可以对参数进行断言以确保命令被正确地执行”。

<details>
<summary>英文:</summary>

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 &quot;executed&quot; correctly.

</details>



# 答案2
**得分**: 2

有一种方法可以做到这一点所有的功劳归功于这篇文章[this](https://npf.io/2015/06/testing-exec-command/)。请查看该文章以了解下面的代码是如何工作的:

```go
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)
}
英文:

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{&quot;-test.run=TestHelperProcess&quot;, &quot;--&quot;, command}
    cs = append(cs, args...)
    cmd := exec.Command(os.Args[0], cs...)
    cmd.Env = []string{&quot;GO_WANT_HELPER_PROCESS=1&quot;}
    return cmd
}

func TestHelperProcess(t *testing.T){
    if os.Getenv(&quot;GO_WANT_HELPER_PROCESS&quot;) != &quot;1&quot; {
        return
    }
    os.Exit(0)
}

答案3

得分: 1

我知道的在Go语言中使用多态性的最佳方法是使用多态。你的想法是正确的。我创建了一个详细的解释,你可以在https://github.com/schollii/go-test-mock-exec-command找到。当我搜索如何模拟os/exec时,我只能找到另一个答案中提到的环境变量技术。这种方法绝对不是必需的,正如我在我提供的git仓库的自述文件中所提到的,只需要一点多态性就可以实现。

总结如下:

  1. 创建一个exec.Cmd的接口类,只包含应用程序(或模块)代码中需要使用的必要方法。
  2. 创建一个实现该接口的结构体,例如可以只提及exec.Cmd
  3. 创建一个指向从步骤2返回的结构体的包级别变量(导出的变量)。
  4. 让你的应用程序代码使用该包级别变量。
  5. 让你的测试创建一个新的实现该接口的结构体,但只包含输出和退出码,并让测试用例将该包级别变量替换为该新结构体的实例。

在应用程序代码中,它看起来像这样:

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

// 在测试中覆盖此函数以模拟git shell命令
var shellCommander = newExecShellCommander

func myFuncThatUsesExecCmd() {
    cmd := shellCommander("git", "rev-parse", "--abbrev-ref", "HEAD")
    err := cmd.Run()
    if err != nil {
        // 处理错误
    } else {
        // 处理输出
    }
}

在测试代码中,它看起来像这样:

type myShellCommand struct {
    RunnerFunc func() error
}

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

func Test_myFuncThatUsesExecCmd(t *testing.T) {
    // 临时替换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
            },
        }
    }

    // 现在shellCommander被模拟了,调用我们想要测试的函数:
    myFuncThatUsesExecCmd()
    // 进行检查
}
英文:

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(&quot;git&quot;, &quot;rev-parse&quot;, &quot;--abbrev-ref&quot;, &quot;HEAD&quot;)
    err := cmd.Run()
    if err != nil {
        // handle error
    } else {
        // process &amp; 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(&quot;exec.Command() for %v called with %v and %v\n&quot;, 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
}

答案4

得分: -3

如何模拟exec.Cmd / exec.Command()

你不能模拟exec.Cmd / exec.Command()。请考虑使用非模拟的测试策略。

英文:

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

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

huangapple
  • 本文由 发表于 2022年2月13日 23:50:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/71102318.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定