【问题标题】:Fill os.Stdin for function that reads from it为从中读取的函数填充 os.Stdin
【发布时间】:2017-09-22 12:49:10
【问题描述】:

如何在我的测试中填写 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()
    /* ... */
}

【问题讨论】:

    标签: testing go command-line automated-tests user-input


    【解决方案1】:

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

    【讨论】:

    • 谢谢,它按预期工作。我有一个问题:当我有 2 个输入(例如姓名和年龄)时,我需要更改什么?我该如何修改测试,以便它使用临时文件中的第二行来处理第二个问题?
    • @Wulthan 如果userInupt() 都读取了 2 个输入,则测试还需要将 2 个输入写入文件,例如content := []byte("Tom\n22")(或content := []byte("Tom 22"),取决于userInput() 读取它们的方式)。
    • 现在可以了。我的设置略有不同,每次都重置扫描仪,这显然不适用于 TempFile。谢谢。
    【解决方案2】:

    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 流。许多实用程序依靠Read() 调用返回的io.EOF 来知道没有更多数据即将到来,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 的输入(除非您已经为此目的准备了一个文件)并捕获stdoutstderr 的输出(请参阅@987654328 @ 以后者为例)。

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        @icza's easy, preferred way的实现:

        还请注意,您可以将 userInput() 重构为不从 os.Stdin 读取, 但相反,它可以接收一个 io.Reader 来读取。这个会 让它更健壮,更容易测试。

        在您的应用程序中,您可以简单地将 os.Stdin 传递给它,并且在测试中您可以 将任何 io.Reader 传递给在测试中创建/准备的它,例如使用 strings.NewReader()bytes.NewBuffer()bytes.NewBufferString()

        hello.go

        package main
        
        import (
            "bufio"
            "fmt"
            "os"
            "io"
        )
        
        func userInput(reader io.Reader) error {
            scanner := bufio.NewScanner(reader)
            var username string
            fmt.Println("What is your name?")
            
            if scanner.Scan() {
                username = scanner.Text()
            }
            if scanner.Err() != nil {
                return scanner.Err()
            }
        
            fmt.Println("Hello", username)
            return nil
        }
        
        func main() {
            userInput(os.Stdin)
        }
        

        hello_test.go

        package main
        
        import (
            "bytes"
            "io"
            "strings"
            "testing"
        )
        
        func TestUserInputWithStringsNewReader(t *testing.T) {
            input := "Tom"
            var reader io.Reader = strings.NewReader(input)
            
            err := userInput(reader)
            if err != nil {
               t.Errorf("Failed to read from strings.NewReader: %w", err)
            }
        }
        
        func TestUserInputWithBytesNewBuffer(t *testing.T) {
            input := "Tom"
            var reader io.Reader = bytes.NewBuffer([]byte(input))
            
            err := userInput(reader)
            if err != nil {
                t.Errorf("Failed to read from bytes.NewBuffer: %w", err)
            }
        }
        
        func TestUserInputWithBytesNewBufferString(t *testing.T) {
            input := "Tom"
            var reader io.Reader = bytes.NewBufferString(input)
            
            err := userInput(reader)
            if err != nil {
               t.Errorf("Failed to read from bytes.NewBufferString: %w", err)
            }
        }
        

        运行程序:

        go run hello.go

        What is your name?
        Tom
        Hello Tom
        

        运行测试:

        go test hello_test.go hello.go -v

        === RUN   TestUserInputWithStringsNewReader
        What is your name?
        Hello Tom
        --- PASS: TestUserInputWithStringsNewReader (0.00s)
        === RUN   TestUserInputWithBytesNewBuffer
        What is your name?
        Hello Tom
        --- PASS: TestUserInputWithBytesNewBuffer (0.00s)
        === RUN   TestUserInputWithBytesNewBufferString
        What is your name?
        Hello Tom
        --- PASS: TestUserInputWithBytesNewBufferString (0.00s)
        PASS
        ok      command-line-arguments  0.141s
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2017-10-12
          • 1970-01-01
          • 2014-12-10
          • 2013-09-16
          • 2016-02-19
          • 2011-07-07
          • 1970-01-01
          相关资源
          最近更新 更多