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

时间:2017-09-22 12:49:10

标签: 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()

    /* ... */
}

现在我如何测试这种情况并模拟用户输入? 以下示例不起作用。斯坦丁仍然是空的。

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

    err := userInput()
    /* ... */
}

3 个答案:

答案 0 :(得分:16)

模拟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
}

这就是你测试它的方法:

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

另请参阅有关模拟文件系统的相关问题:Example code for testing the filesystem in Golang

简单易行的方式

另请注意,您可以重构userInput()以便不从os.Stdin读取,而是可以接收io.Reader来读取。这将使其更加强大,并且更容易测试。

在您的应用中,您只需将os.Stdin传递给它,在测试中,您可以将任何io.Reader传递给在测试中创建/准备的任何内容,例如使用strings.NewReader()bytes.NewBuffer()bytes.NewBufferString()

答案 1 :(得分:0)

os.Pipe()

最简单的解决方案是使用os.Pipe(),而不是弄乱实际的文件系统以及在存储设备上对真实文件进行读写操作。

示例

您的userInput()的代码确实必须进行调整,而@icza's solution确实可以做到这一点。但是测试本身应该更像这样:

func Test_userInput(t *testing.T) {
    input := []byte("Alice")
    r, w, err := os.Pipe()
    if err != nil {
        t.Fatal(err)
    }

    _, err = w.Write(input)
    if err != nil {
        t.Error(err)
    }
    w.Close()

    stdin := os.Stdin
    // Restore stdin right after the test.
    defer func() { os.Stdin = stdin }()
    os.Stdin = r

    if err = userInput(); err != nil {
        t.Fatalf("userInput: %v", err)
    }
}

详细信息

有关此代码的几点要点:

  1. 写完后,始终关闭w流。许多实用程序都依靠io.EOF调用返回的Read()来知道不会再有数据了,bufio.Scanner也不例外。如果不关闭流,则scanner.Scan()调用将永远不会返回,而是保持内部循环并等待更多输入,直到程序被强制终止(如测试超时)。

  2. 管道缓冲区的容量因系统而异,如post in the Unix & Linux Stack Exchange中所详细讨论的那样,因此,如果模拟输入的大小可能超过该大小,则应将写入的内容包装在goroutine中像这样:

    //...
    go func() {
        _, err = w.Write(input)
        if err != nil {
            t.Error(err)
        }
        w.Close()
    }()
    //...
    

    这可以防止在管道已满并且必须等待它开始清空时发生死锁,但是原本应该从管道中读取并清空管道的代码(在这种情况下为userInput())不会启动,因为写作还没有结束。

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

    这样的测试看起来与标称情况几乎相同,只是您不在管道的w端写任何东西,只需闭合r端,您实际上就期望并想要userInput()返回error。而且,当您具有相同功能的两个或多个测试几乎相同时,通常是将它们实现为单个table driven test的好时机。有关示例,请参见Go playground

io.Reader

userInput()的例子很简单,您可以(并且应该)对其进行重构,并从io.Reader读取类似的情况,就像@icza suggests(请参阅the playground )。

您应该始终努力依赖某种形式的依赖项注入,而不要依赖全局状态(在这种情况下,os.Stdinos包中的全局变量),因为这样可以更好地控制调用代码来确定被调用代码的行为,这对单元测试至关重要,并且通常可以促进更好的代码重用。

返回os.Pipe()

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

的地方

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

答案 2 :(得分:0)

您可以使用 *bufio.Scanner 抽象 io.Stdinio.Writer 抽象 io.Stdout,同时将它们作为依赖项传递给您的结构,请参阅 要点:https://gist.github.com/antonzhukov/2a6749f780b24f38b08c9916caa96663 和 游乐场:https://play.golang.org/p/BZMqpACupSc