英文:
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()
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论