如何在Go语言中为多个单元测试模拟exec.Command?

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

How to mock exec.Command for multiple unit tests in Go lang?

问题

我刚刚学习了使用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中更新?

英文:

I just learnt unit testing functions that uses exec.Command() i.e., mocking exec.Command(). I went ahead to added more unit cases, but running into issues of not able to mock the output for different scenarios.

Here is a sample code hello.go I'm trying to test...

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

Below is the test code 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 fails for the second 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

Even though I'm trying to set the global mockedDate value inside the test case, it's still getting the global value that it was initialized with. Is the global value not getting set? Or the changes to that global var is not getting updated in TestHelperProcess?

答案1

得分: 6

我找到了解决方案...

全局变量没有被设置吗?还是对该全局变量的更改没有在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
英文:

I got the solution for this...

> Is the global value not getting set? Or the changes to that global var is not getting updated in TestHelperProcess?

Since in TestPrintDate(), fakeExecCommand is called instead of exec.Command, and calling fakeExecCommand runs go test to run only TestHelperProcess(), it's altogether a new invocation where only TestHelperProcess() will be executed. Since only TestHelperProcess() is called, the global variables aren't being set.

The solution would be to set the Env in the fakeExecCommand, and retrieve that in TestHelperProcess() and return those values.

PS> TestHelperProcess is renamed to TestExecCommandHelper, And few variables are renamed.

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 results as below...
(Purposely failing one test to show that the mock is working properly).

 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

得分: 0

根据你发布的代码,mockedDate 变量没有起到任何作用。测试和对 printDate() 的调用都没有使用它,所以 TestPrintDateUnableToRunError() 测试的执行结果与之前的测试相同。

如果你想要在 printDate() 函数中添加功能,以返回字符串 "无法运行日期命令"(当情况如此时),那么你在第62行的条件将会满足。不过,当从 printDate() 返回的错误不为 nil 时,这样的检查应该是不必要的。如果返回的错误不为 nil,那么预期输出字符串应该是无效的(或为空字符串 "")。

我无法确定你真正希望 printDate() 失败的方式,但就目前而言,它无法返回你在 TestPrintDateUnableToRunError() 中期望的值。

英文:

Based on the code you've posted, the mockedDate variable doesn't do anything. Neither the test, nor the call to printDate() are utilizing it, so the TestPrintDateUnableToRunError() test performs just like the tests before it.

If you were to add functionality to the printDate() function to return string of "Unable to run date command" (when that is the case), then your condition on line 62 would pass. That said, such checks should be unnecessary, when you have an error in the return values from printDate(). If the returned error is non-nil, the returned output string should be expected to be invalid (or empty, "").

I can't tell how you really want printDate() to fail, but as it stands, there's no way for it to return the values you're expecting in TestPrintDateUnableToRunError().

huangapple
  • 本文由 发表于 2017年8月21日 11:58:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/45789101.html
匿名

发表评论

匿名网友

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

确定