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

问题描述 投票:0回答:5

我需要嘲笑

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")
}
go mocking
5个回答
4
投票

虽然劫持测试可执行文件来运行特定函数是有效的,但使用常规依赖注入会更直接。不需要魔法。

设计一个可以运行命令的界面(例如

CommandExecutor
),然后将其中之一作为运行命令所需的任何函数的输入。然后,您可以在测试期间提供满足接口的模拟实现(手工制作或使用您选择的工具(如 GoMock)生成)。为您的生产代码提供真正的实现(调用
exec
包)。您的模拟实现甚至可以对参数进行断言,以便您知道命令正在正确“执行”。


3
投票

实际上有一种方法可以做到这一点。所有功劳都归功于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)
}

1
投票

据我所知,Go 中最好的方法是使用多态性。你走在正确的轨道上。详细的解释位于 https://github.com/schollii/go-test-mock-exec-command,我创建它是因为当我搜索如何模拟

os/exec
时,我所能找到的只是 env 变量另一个答案中提到的技术。这种方法绝对没有必要,正如我在我链接的 git 存储库的自述文件中提到的,它所需要的只是一点多态性。

总结基本上是这样的:

  1. exec.Cmd
    创建一个接口类,该类仅包含应用程序(或模块)代码使用的必要方法
  2. 创建一个实现该接口的结构,例如它可以只提及
    exec.Cmd
  3. 创建一个包级 var(导出),指向返回步骤 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}
}

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

0
投票

为了扩展 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")
}

-3
投票

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

你不能。提出一个非基于模拟的测试策略。

© www.soinside.com 2019 - 2024. All rights reserved.