Go语言 如何模拟 * exec.cmd/exec.command()?

xeufq47z  于 2022-12-07  发布在  Go
关注(0)|答案(4)|浏览(117)

我需要嘲笑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()
如果我返回一个“mock”命令,mock函数就可以工作,尽管我更喜欢控制Run()函数:

var rName string
var rArgs []string

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

    return exec.Command("echo")
}
scyqe7ek

scyqe7ek1#

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

9lowa7mx2#

虽然劫持测试可执行文件来运行一个特定的函数是可行的,但使用常规的依赖注入会更直接。
设计界面(例如CommandExecutor),然后将其中一个作为运行命令所需函数的输入。(手工制作或使用您选择的工具生成,比如GoMock)。提供真实的的实现(它调用exec包)。您的模拟实现甚至可以对参数进行Assert,这样您就知道命令正在正确地“执行”。

x7yiwoj4

x7yiwoj43#

我所知道的在go中最好的方法是使用多态性。你的思路是对的。详细的解释在https://github.com/schollii/go-test-mock-exec-command,我创建它是因为当我搜索如何模仿os/exec时,我所能找到的只是在另一个答案中提到的env变量技术。这种方法是绝对没有必要的,正如我在链接到的git repo的自述文件中提到的,它所需要的只是一点多态性。
总结基本上是这样的:
1.为exec.Cmd创建一个接口类,它只包含应用程序(或模块)代码使用的必要方法
1.创建一个实现该接口的结构,例如它可以只提到exec.Cmd
1.创建一个包级var(已导出),它指向一个返回步骤2中的结构的函数
1.使应用程序代码使用该包级变量
1.使测试创建一个实现该接口的新结构,但只包含输出和退出代码,并使测试用该新结构的示例替换该包级var
它在应用程序代码中看起来如下所示:

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

xytpbqjk4#

如何模拟 * exec.cmd/exec.command()?
你不能。想出一个非基于模拟的测试策略。

相关问题