Wul*_*han 9 testing command-line automated-tests user-input go
如何在我的测试中填写os.Stdin以获取使用扫描仪从中读取的函数?
我使用以下功能通过扫描仪请求用户命令行输入:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
println("What is your name?")
scanner.Scan()
username = scanner.Text()
/* ... */
}
Run Code Online (Sandbox Code Playgroud)
现在我该如何测试这种情况并模拟用户输入?以下示例不起作用.斯坦丁仍然是空的.
func TestUserInput(t *testing.T) {
var file *os.File
file.Write([]byte("Tom"))
os.Stdin = file
err := userInput()
/* ... */
}
Run Code Online (Sandbox Code Playgroud)
icz*_*cza 20
os.Stdin您在正确的轨道上os.Stdin是*os.File可以修改的变量(类型),您可以在测试中为其分配新值.
最简单的方法是创建一个临时文件,其中包含您要模拟的内容作为输入os.Stdin.要创建临时文件,请使用ioutil.TempFile().然后将内容写入其中,并回到文件的开头.现在您可以将其设置为os.Stdin并执行测试.不要忘记清理临时文件.
我修改你userInput()的这个:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("What is your name?")
var username string
if scanner.Scan() {
username = scanner.Text()
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Println("Entered:", username)
return nil
}
Run Code Online (Sandbox Code Playgroud)
这就是你如何测试它:
func TestUserInput(t *testing.T) {
content := []byte("Tom")
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
log.Fatal(err)
}
if _, err := tmpfile.Seek(0, 0); err != nil {
log.Fatal(err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }() // Restore original Stdin
os.Stdin = tmpfile
if err := userInput(); err != nil {
t.Errorf("userInput failed: %v", err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
}
Run Code Online (Sandbox Code Playgroud)
运行测试,我们看到一个输出:
What is your name?
Entered: Tom
PASS
Run Code Online (Sandbox Code Playgroud)
另请参阅有关模拟文件系统的相关问题:在Golang中测试文件系统的示例代码
另请注意,您可以重构userInput()不读取os.Stdin,但它可以接收io.Reader读取.这将使其更加健壮并且更容易测试.
在您的应用程序中,您可以简单地传递os.Stdin给它,并且在测试中,您可以将任何内容传递给在测试中io.Reader创建/准备的任何内容,例如使用strings.NewReader(),bytes.NewBuffer()或bytes.NewBufferString().
ant*_*ris 10
os.Pipe()最简单的解决方案是使用os.Pipe().
你的代码userInput()确实需要调整,@icza\'s 的解决方案确实可以达到这个目的。但测试本身应该更像这样:
func Test_userInput(t *testing.T) {\n input := []byte("Alice")\n r, w, err := os.Pipe()\n if err != nil {\n t.Fatal(err)\n }\n\n _, err = w.Write(input)\n if err != nil {\n t.Error(err)\n }\n w.Close()\n\n // Restore stdin right after the test.\n defer func(v *os.File) { os.Stdin = v }(os.Stdin)\n os.Stdin = r\n\n if err = userInput(); err != nil {\n t.Fatalf("userInput: %v", err)\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n这段代码有几个要点:
\nw写完后请务必关闭流。许多实用程序依靠io.EOF调用返回来Read()知道没有更多数据传入,也不bufio.Scanner例外。如果您不关闭流,您的scanner.Scan()调用将永远不会返回,而是继续在内部循环并等待更多输入,直到程序被强制终止(如测试超时时)。
管道缓冲区容量因系统而异,正如Unix & Linux Stack Exchange 中的一篇文章中详细讨论的那样,因此如果模拟输入的大小可能超过该容量,则应该将写入包装在像这样的 goroutine 中:
\n//...\ngo func() {\n _, err = w.Write(input)\n if err != nil {\n t.Error(err)\n }\n w.Close()\n}()\n//...\nRun Code Online (Sandbox Code Playgroud)\n当管道已满并且写入必须等待它开始清空时,这可以防止死锁,但是应该读取和清空管道的代码(userInput()在本例中)没有启动,因为写入没有被启动还没有结束。
测试还应该验证错误是否得到正确处理,在本例中,由userInput(). 这意味着您必须找出一种方法使调用scanner.Err()在测试中返回错误。一种方法可能是r在它有机会之前关闭它应该正在读取的流。
这样的测试看起来几乎与标称情况相同,只是您不在w管道末尾写入任何内容,只需关闭r末尾,并且您实际上期望并想要userInput()返回一个error. 当您对同一功能有两个或多个几乎相同的测试时,通常是将它们实现为单个表驱动测试的好时机。有关示例,请参阅Go 游乐场。
io.Reader的示例userInput()非常简单,您可以(并且应该)重构它以及类似的案例以从 an 中读取io.Reader,就像@icza 建议的那样(请参阅游乐场)。
您应该始终努力依赖某种形式的依赖项注入而不是全局状态(os.Stdin在本例中,是包中的全局变量os),因为这可以为调用代码提供更多控制,以确定被调用的代码片段的行为方式,这对于单元测试至关重要,并且总体上有助于更好的代码重用。
os.Pipe()在某些情况下,您可能无法真正更改函数来获取注入的依赖项,就像您必须测试main()Go 可执行文件的函数一样。那么,改变测试中的全局状态(并希望最终能够正确恢复它而不影响后续测试)是您唯一的选择。这就是我们回到的地方os.Pipe()
测试时main(),请使用os.Pipe()来模拟输入stdin(除非您已经有为此目的准备的文件)并捕获stdout和的输出stderr(有关后者的示例,请参阅游乐场)。