填写os.Stdin以获取从中读取的函数

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

\n

最简单的解决方案是使用os.Pipe().

\n

例子

\n

你的代码userInput()确实需要调整,@icza\'s 的解决方案确实可以达到这个目的。但测试本身应该更像这样:

\n
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}\n
Run Code Online (Sandbox Code Playgroud)\n

细节

\n

这段代码有几个要点:

\n
    \n
  1. w写完后请务必关闭流。许多实用程序依靠io.EOF调用返回来Read()知道没有更多数据传入,也不bufio.Scanner例外。如果您不关闭流,您的scanner.Scan()调用将永远不会返回,而是继续在内部循环并等待更多输入,直到程序被强制终止(如测试超时时)。

    \n
  2. \n
  3. 管道缓冲区容量因系统而异,正如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//...\n
    Run Code Online (Sandbox Code Playgroud)\n

    当管道已满并且写入必须等待它开始清空时,这可以防止死锁,但是应该读取和清空管道的代码(userInput()在本例中)没有启动,因为写入没有被启动还没有结束。

    \n
  4. \n
  5. 测试还应该验证错误是否得到正确处理,在本例中,由userInput(). 这意味着您必须找出一种方法使调用scanner.Err()在测试中返回错误。一种方法可能是r在它有机会之前关闭它应该正在读取的流。

    \n

    这样的测试看起来几乎与标称情况相同,只是您不在w管道末尾写入任何内容,只需关闭r末尾,并且您实际上期望并想要userInput()返回一个error. 当您对同一功能有两个或多个几乎相同的测试时,通常是将它们实现为单个表驱动测试的好时机。有关示例,请参阅Go 游乐场。

    \n
  6. \n
\n

io.Reader

\n

的示例userInput()非常简单,您可以(并且应该)重构它以及类似的案例以从 an 中读取io.Reader,就像@icza 建议的那样(请参阅游乐场)。

\n

您应该始终努力依赖某种形式的依赖项注入而不是全局状态(os.Stdin在本例中,是包中的全局变量os),因为这可以为调用代码提供更多控制,以确定被调用的代码片段的行为方式,这对于单元测试至关重要,并且总体上有助于更好的代码重用。

\n

返回os.Pipe()

\n

在某些情况下,您可能无法真正更改函数来获取注入的依赖项,就像您必须测试main()Go 可执行文件的函数一样。那么,改变测试中的全局状态(并希望最终能够正确恢复它而不影响后续测试)是您唯一的选择。这就是我们回到的地方os.Pipe()

\n

测试时main(),请使用os.Pipe()来模拟输入stdin(除非您已经有为此目的准备的文件)并捕获stdout和的输出stderr(有关后者的示例,请参阅游乐场)。

\n