如何对用golang编写的hintui包进行单元测试?

I am new to golang and I am using an interactive prompt called promptui (https://github.com/manifoldco/promptui) in a project of mine. I have written several unit tests for this project already but I am struggling with how I would unit test this particular package that requires an input.

For example, How would I go about testing the following lines of code (encapsulated in a function):

func setEmail() string {
  prompt := promptui.Prompt{Label: "Input your Email",
     Validate: emailValidations,
  }

  email, err := prompt.Run()
  if err != nil {
    color.red("failed getting email")
    os.exit(3)
  }
  return email
}

I think I need to somehow mock stdin but can't figure out the best way to do that within a test.

For whatever reason, they don't export their stdin interface (https://github.com/manifoldco/promptui/blob/master/prompt.go#L49), so you can't mock it out, but you can directly mock os.Stdin and prefill it with whatever you need for testing. Though I agree with @Adrian, it has its own tests, so this shouldn't be necessary.

Extracted and refactored/simplified from source: Fill os.Stdin for function that reads from it

Refactored this way, it can be used for any function that reads from os.Stdin and expects a specific string.

Playground link: https://play.golang.org/p/rjgcGIaftBK

func TestSetEmail(t *testing.T) {
    if err := TestExpectedStdinFunc("email@test.com", setEmail); err != nil {
        t.Error(err)
        return
    }
    fmt.Println("success")
}

func TestExpectedStdinFunc(expected string, f func() string) error {
    content := []byte(expected)
    tmpfile, err := ioutil.TempFile("", "example")
    if err != nil {
        return err
    }

    defer os.Remove(tmpfile.Name()) // clean up

    if _, err := tmpfile.Write(content); err != nil {
        return err
    }

    if _, err := tmpfile.Seek(0, 0); err != nil {
        return err
    }

    oldStdin := os.Stdin
    defer func() { os.Stdin = oldStdin }() // Restore original Stdin

    os.Stdin = tmpfile
    actual := f()
    if actual != expected {
        return errors.New(fmt.Sprintf("test failed, exptected: %s actual: %s", expected, actual))
    }

    if err := tmpfile.Close(); err != nil {
        return err
    }
    return nil
}

You should not try to test promptui as it is expected to be tested by its author.

What you can test:

  1. You send correct parameters when you create promptui.Prompt
  2. You use that promptui.Prompt in your code
  3. You properly handle promptui.Prompt results

As you can see, all these tests does not verify if promptui.Prompt works correctly inside.

Tests #2 and #3 could be combined. You need to run you code against mock and if you got correct result, you can believe that both #2 and #3 are correct.

Create mock:

type Runner interface {
    Run() (int, string, error)
}

type promptMock struct {
    // t is not required for this test, but it is would be helpful to assert input parameters if we have it in Run()
    t *testing.T
}

func (p promptMock) Run() (int, string, error) {
    // return expected result
    return 1, "", nil
}

You will need separate mock for testing error flow.

Update your code to inject mock:

func setEmail(runner Runner) string {
    email, err := runner.Run()
    if err != nil {
      color.red("failed getting email")
      os.exit(3)
    }
    return email
}

Now it is testable.

Create function that creates prompt:

func getRunner() promptui.Prompt {
  return promptui.Prompt{Label: "Input your Email",
     Validate: emailValidations,
  }
} 

Write simple assert test to verify that we create correct structure.

The only not tested line will be setEmail(getRunner()) but it is trivial and can be covered by other types of tests.