用于标准输入测试的 Golang 模式

Golang patterns for stdin testing

编辑:Adrian 的建议很有道理,所以我将我的代码移到一个函数中并从我的 cobra 块中调用该函数:

package cmd

import (
    "fmt"
    "log"
    "os"
    "io"

    "github.com/spf13/cobra"
    "github.com/spf13/viper"
    input "github.com/tcnksm/go-input"
)

var configureCmd = &cobra.Command{
    Use:   "configure",
    Short: "Configure your TFE credentials",
    Long:  `Prompts for your TFE API credentials, then writes them to
    a configuration file (defaults to ~/.tgc.yaml`,
    Run: func(cmd *cobra.Command, args []string) {
        CreateConfigFileFromPrompts(os.Stdin, os.Stdout)
    },
}

func CreateConfigFileFromPrompts(stdin io.Reader, stdout io.Writer) {
    ui := &input.UI{
        Writer: stdout,
        Reader: stdin,
    }

    tfeURL, err := ui.Ask("TFE URL:", &input.Options{
        Default:  "https://app.terraform.io",
        Required: true,
        Loop:     true,
        })
    if err != nil {
        log.Fatal(err)
    }
    viper.Set("tfe_url", tfeURL)

    tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
        Default:     "",
        Required:    true,
        Loop:        true,
        Mask:        true,
        MaskDefault: true,
        })

    if err != nil {
        log.Fatal(err)
    }
    viper.Set("tfe_api_token", tfeAPIToken)

    configPath := ConfigPath()
    viper.SetConfigFile(configPath)

    err = viper.WriteConfig()

    if err != nil {
        log.Fatal("Failed to write to: ", configPath, " Error was: ", err)
    }

    fmt.Println("Saved to", configPath)
}

那么我可以将什么传递给此方法来测试输出是否符合预期?

package cmd

import (
  "strings"
  "testing"
)

func TestCreateConfigFileFromPrompts(t *testing.T) {
  // How do I pass the stdin and out to the method?
  // Then how do I test their contents?
  // CreateConfigFileFromPrompts()
}

reader和writer需要在被测函数调用前设置好。调用后,将结果写入writer中进行验证。

package cmd

import (
  "strings"
  "testing"
)

func TestCreateConfigFileFromPrompts(t *testing.T) {
  in := strings.NewReader("<your input>") // you can use anything that satisfies io.Reader interface here
  out := new(strings.Builder) // you could use anything that satisfies io.Writer interface here like bytes.Buffer

  CreateConfigFileFromPrompts(in, out)

  // here you verify the output written into the out
  expectedOutput := "<your expected output>"
  if out.String() != expectedOutput {
    t.Errorf("expected %s to be equal to %s", out.String(), expectedOutput)
  }
}
func TestCreateConfigFileFromPrompts(t *testing.T) {

    var in bytes.Buffer
    var gotOut, wantOut bytes.Buffer

    // The reader should read to the \n each of two times.
    in.Write([]byte("example-url.com\nexampletoken\n"))

    // wantOut could just be []byte, but for symmetry's sake I've used another buffer
    wantOut.Write([]byte("TFE URL:TFE API Token (Create one at example-url.com/app/settings/tokens)"))

    // I don't know enough about Viper to manage ConfigPath()
    // but it seems youll have to do it here somehow.
    configFilePath := "test/file/location"

    CreateConfigFileFromPrompts(&in, &gotOut)

    // verify that correct prompts were sent to the writer
    if !bytes.Equal(gotOut.Bytes(), wantOut.Bytes()) {
        t.Errorf("Prompts = %s, want %s", gotOut.Bytes(), wantOut.Bytes())
    }

    // May not need/want to test viper's writing of the config file here, or at all, but if so:
    var fileGot, fileWant []byte
    fileWant = []byte("Correct Config file contents:\n URL:example-url.com\nTOKEN:exampletoken")
    fileGot, err := ioutil.ReadFile(configFilePath)
    if err != nil {
        t.Errorf("Error reading config file %s", configFilePath)
    }
    if !bytes.Equal(fileGot, fileWant) {
        t.Errorf("ConfigFile: %s not created correctly got = %s, want %s", configFilePath, fileGot, fileWant)
    }
}

正如@zdebra 在对他的回答的评论中强调的那样,go-input 包出现恐慌并给您错误:Reader 必须是一个文件。如果你已经习惯使用那个包,你可以通过禁用 ui.Ask 上的屏蔽选项来避免这个问题作为你的第二个输入:

tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
        Default:     "",
        Required:    true,
        Loop:        true,
        //Mask:        true, // if this is set to True, the input must be a file for some reason
        //MaskDefault: true,
    })