【问题标题】:How to mock exec.Command for multiple unit tests in Go lang?如何在 Go 语言中模拟 exec.Command 进行多个单元测试?
【发布时间】:2017-08-21 03:58:51
【问题描述】:

我刚刚学习了使用exec.Command() 的单元测试功能,即模拟exec.Command()。我继续添加更多单元案例,但遇到了无法模拟不同场景的输出的问题。

这是一个示例代码hello.go我正在尝试测试...

package main

import (
    "fmt"
    "os/exec"
)

var execCommand = exec.Command

func printDate() ([]byte, error) {
    cmd := execCommand("date")
    out, err := cmd.CombinedOutput()
    return out, err
}

func main() {
    fmt.Printf("hello, world\n")
    fmt.Println(printDate())
}

下面是测试代码hello_test.go...

package main

import (
    "fmt"
    "os"
    "os/exec"
    "testing"
)

var mockedExitStatus = 1
var mockedDate = "Sun Aug 20"
var expDate = "Sun Aug 20"

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
    }

    // println("Mocked Data:", mockedDate)
    fmt.Fprintf(os.Stdout, mockedDate)
    os.Exit(mockedExitStatus)
}

func TestPrintDate(t *testing.T) {
    execCommand = fakeExecCommand
    defer func() { execCommand = exec.Command }()

    out, err := printDate()
    print("Std out: ", string(out))
    if err != nil {
        t.Errorf("Expected nil error, got %#v", err)
    }
    if string(out) != expDate {
        t.Errorf("Expected %q, got %q", expDate, string(out))
    }
}

func TestPrintDateUnableToRunError(t *testing.T) {
    execCommand = fakeExecCommand
    defer func() { execCommand = exec.Command }()

    mockedExitStatus = 1
    mockedDate = "Unable to run date command"
    expDate = "Unable to run date command"

    out, err := printDate()
    print("Std out: ", string(out))
    if err != nil {
        t.Errorf("Expected nil error, got %#v", err)
    }
    if string(out) != expDate {
        t.Errorf("Expected %q, got %q", expDate, string(out))
    }
}

go test 第二次测试失败TestPrintDateUnableToRunError...

$ go test hello
Std out: Sun Aug 20Std out: Sun Aug 20--- FAIL: TestPrintDateTomorrow (0.01s)
    hello_test.go:62: Expected "Unable to run date command", got "Sun Aug 20"
FAIL
FAIL    hello   0.017s

即使我试图在测试用例中设置全局 mockedDate 值,它仍然获得初始化时使用的全局值。 没有设置全局值吗?或者该全局变量的更改没有在TestHelperProcess 中更新?

【问题讨论】:

    标签: unit-testing go


    【解决方案1】:

    我找到了解决方案...

    没有设置全局值吗?或者对该全局变量的更改没有在 TestHelperProcess 中更新?

    由于在TestPrintDate() 中调用fakeExecCommand 而不是exec.Command,并且调用fakeExecCommand 运行go test 以仅运行TestHelperProcess(),这完全是一个新的调用,其中仅会执行TestHelperProcess()。由于只调用了TestHelperProcess(),因此没有设置全局变量。

    解决方案是在fakeExecCommand 中设置Env,然后在TestHelperProcess() 中检索它并返回这些值。

    PS> TestHelperProcess 被重命名为 TestExecCommandHelper,并且很少有变量被重命名。

    package main
    
    import (
        "fmt"
        "os"
        "os/exec"
        "strconv"
        "testing"
    )
    
    var mockedExitStatus = 0
    var mockedStdout string
    
    func fakeExecCommand(command string, args ...string) *exec.Cmd {
        cs := []string{"-test.run=TestExecCommandHelper", "--", command}
        cs = append(cs, args...)
        cmd := exec.Command(os.Args[0], cs...)
        es := strconv.Itoa(mockedExitStatus)
        cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1",
            "STDOUT=" + mockedStdout,
            "EXIT_STATUS=" + es}
        return cmd
    }
    
    func TestExecCommandHelper(t *testing.T) {
        if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
            return
        }
    
        // println("Mocked stdout:", os.Getenv("STDOUT"))
        fmt.Fprintf(os.Stdout, os.Getenv("STDOUT"))
        i, _ := strconv.Atoi(os.Getenv("EXIT_STATUS"))
        os.Exit(i)
    }
    
    func TestPrintDate(t *testing.T) {
        mockedExitStatus = 1
        mockedStdout = "Sun Aug 201"
        execCommand = fakeExecCommand
        defer func() { execCommand = exec.Command }()
        expDate := "Sun Aug 20"
    
        out, _ := printDate()
        if string(out) != expDate {
            t.Errorf("Expected %q, got %q", expDate, string(out))
        }
    }
    
    func TestPrintDateUnableToRunError(t *testing.T) {
        mockedExitStatus = 1
        mockedStdout = "Unable to run date command"
        execCommand = fakeExecCommand
        defer func() { execCommand = exec.Command }()
    
        expDate := "Unable to run date command"
    
        out, _ := printDate()
        // println("Stdout: ", string(out))
        if string(out) != expDate {
            t.Errorf("Expected %q, got %q", expDate, string(out))
        }
    }
    

    go test 结果如下... (故意未能通过一项测试以表明模拟工作正常)。

     go test hello
    --- FAIL: TestPrintDate (0.01s)
            hello_test.go:45: Expected "Sun Aug 20", got "Sun Aug 201"
    FAIL
    FAIL    hello   0.018s
    

    【讨论】:

      【解决方案2】:

      根据您发布的代码,mockedDate 变量没有任何作用。测试和对printDate() 的调用都没有使用它,因此TestPrintDateUnableToRunError() 测试的执行与之前的测试一样。

      如果您要向printDate() 函数添加功能以返回“无法运行日期命令”字符串(在这种情况下),那么您在第 62 行的条件将通过。也就是说,当您在来自printDate() 的返回值中有错误时,此类检查应该是不必要的。如果返回的错误不为零,则返回的输出字符串应该是无效的(或为空,"")。

      我不知道你真的希望printDate() 失败,但就目前而言,它无法返回你期望在TestPrintDateUnableToRunError() 中的值。

      【讨论】:

      • 我实际上是在尝试调用一些脚本,而该脚本有时可能不存在...为了在这里提问,我只是以日期为例...谢谢。顺便一提。后来我意识到我可以通过使用 ENV 变量来做到这一点。
      猜你喜欢
      • 2014-09-07
      • 2018-04-04
      • 2021-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-08-19
      • 1970-01-01
      相关资源
      最近更新 更多