Testing 为从中读取的函数填充os.Stdin

Testing 为从中读取的函数填充os.Stdin,testing,go,command-line,automated-tests,user-input,Testing,Go,Command Line,Automated Tests,User Input,对于使用扫描仪读取的函数,如何在测试中填充os.Stdin 我使用以下功能通过扫描仪请求用户命令行输入: func userInput() error { scanner := bufio.NewScanner(os.Stdin) println("What is your name?") scanner.Scan() username = scanner.Text() /* ... */ } 现在我如何测试这个案例并模拟用户输入? 下面的示例不起作

对于使用扫描仪读取的函数,如何在测试中填充os.Stdin

我使用以下功能通过扫描仪请求用户命令行输入:

func userInput() error {
    scanner := bufio.NewScanner(os.Stdin)

    println("What is your name?")
    scanner.Scan()
    username = scanner.Text()

    /* ... */
}
现在我如何测试这个案例并模拟用户输入? 下面的示例不起作用。Stdin仍然是空的

func TestUserInput(t *testing.T) {
    var file *os.File
    file.Write([]byte("Tom"))
    os.Stdin = file

    err := userInput()
    /* ... */
}
模拟
os.Stdin
您正处于正确的轨道上,
os.Stdin
是一个可以修改的变量(类型),您可以在测试中为它指定一个新值

最简单的方法是创建一个临时文件,其中包含要模拟的内容作为
os.Stdin
上的输入。要创建临时文件,请使用。然后将内容写入其中,并返回到文件的开头。现在您可以将其设置为
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
}
这就是你测试它的方法:

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)
    }
}
运行测试时,我们会看到一个输出:

What is your name?
Entered: Tom
PASS
另请参见有关模拟文件系统的相关问题:

最简单、最受欢迎的方式 还请注意,您可以重构
userInput()
以不从
os.Stdin
读取,但它可以接收一个
io.Reader
来读取。这将使它更健壮,更易于测试

在您的应用程序中,您只需将
os.Stdin
传递给它,在测试中,您可以将在测试中创建/准备的任何
io.Reader
传递给它,例如使用或。

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

例子 您的
userInput()
的代码确实需要调整,而且确实需要调整。但测试本身应该更像这样:

func Test\u userInput(t*testing.t){
输入:=[]字节(“Alice”)
r、 w,err:=os.Pipe()
如果错误!=零{
t、 致命的(错误)
}
_,err=w.Write(输入)
如果错误!=零{
t、 错误(err)
}
w、 关闭()
stdin:=os.stdin
//测试后立即恢复stdin。
defer func(){os.Stdin=Stdin}()
os.Stdin=r
如果err=userInput();err!=nil{
t、 Fatalf(“用户输入:%v”,错误)
}
}
细节 关于此代码,有几个要点:

  • 完成编写后,始终关闭
    w
    流。许多实用程序依赖于
    Read()
    调用返回的
    io.EOF
    ,以知道不再有数据出现,而
    bufio.Scanner
    也不例外。如果不关闭流,您的
    scanner.Scan()
    调用将永远不会返回,但会在内部循环并等待更多输入,直到程序被强制终止(如测试超时)

  • 管道缓冲区容量因系统而异,如a中详细讨论的,因此,如果模拟输入的大小可能超过此值,则应将写入内容包装在GOROUTE中,如下所示:

    /。。。
    go func(){
    _,err=w.Write(输入)
    如果错误!=零{
    t、 错误(err)
    }
    w、 关闭()
    }()
    //...
    
    这可以防止在管道已满且写入操作必须等待管道开始清空时出现死锁,但应该从管道中读取并清空的代码(
    userInput()
    )没有启动,因为写入操作尚未结束

  • 测试还应验证是否正确处理了错误,在这种情况下,错误由
    userInput()
    返回。这意味着您必须找到一种方法,使
    scanner.Err()
    调用在测试中返回错误。一种方法是在它有机会之前关闭它应该读取的
    r

    这样的测试看起来与标称情况几乎相同,只是您不在管道的
    w
    端写入任何内容,只需关闭
    r
    端,您实际上期望并希望
    userInput()
    返回
    错误。当您有两个或多个几乎相同的相同函数的测试时,通常是将它们作为单个函数实现的好时机。有关示例,请参见

  • io.Reader
    userInput()
    的示例非常简单,您可以(也应该)重构它和类似的案例,以便从
    io.Reader
    读取,就像(请参阅)

    您应该始终努力依赖某种形式的依赖注入,而不是全局状态(
    os.Stdin
    ,在这种情况下,是
    os
    包中的一个全局变量),因为这为调用代码提供了更多的控制,以确定被调用代码段的行为,这对于单元测试至关重要,并且通常有助于更好的代码重用

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


    在测试
    main()
    时,一定要使用
    os.Pipe()
    来模拟
    stdin
    的输入(除非您已经为此准备了一个文件),并捕获
    stdout
    stderr
    的输出(请参阅后者的示例)。

    谢谢,它按预期工作。我有一个问题:当我有两个输入(例如姓名和年龄)时,我需要更改什么?我必须如何修改测试,以便它使用tempfile中的第二行来回答第二个问题?@Wulthan如果这两个输入都由
    userinut()
    读取,那么测试还需要向文件写入两个输入,例如
    内容:=[]字节(“Tom\n22”)
    (或
    内容:=[]字节(“Tom 22”)
    ,具体取决于
    userInput()的方式)
    读取它们)。现在我