17

我想要一个单元测试来验证特定的命令行标志是否在枚举中。

这是我想编写测试的代码:

var formatType string

const (
    text = "text"
    json = "json"
    hash = "hash"
)

func init() {
    const (
        defaultFormat = "text"
        formatUsage   = "desired output format"
    )

    flag.StringVar(&formatType, "format", defaultFormat, formatUsage)
    flag.StringVar(&formatType, "f", defaultFormat, formatUsage+" (shorthand)")

}

func main() {
    flag.Parse()
}

仅当 -format 等于上面给出的 const 值之一时,所需的测试才会通过。该值将在 formatType 中可用。一个正确的调用示例是:program -format text

测试所需行为的最佳方法是什么?

注意:也许我表达得不好,但是显示的代码不是单元测试本身,而是我要编写单元测试的代码。这是我正在编写的工具中的一个简单示例,并想询问是否有一种好方法来测试该工具的有效输入。

4

4 回答 4

14

可以通过包flag.Var中的功能实现对标志的自定义测试和处理flag

Flag.Var “用指定的名称和使用字符串定义一个标志。标志的类型和值由第一个参数表示,类型为 Value,通常包含用户定义的 Value 实现。”

Aflag.Value是任何满足Value接口的类型,定义为:

type Value interface {
    String() string
    Set(string) error
}

包源码的example_test.go文件中有一个很好的例子flag

对于您的用例,您可以使用以下内容:

package main

import (
    "errors"
    "flag"
    "fmt"
)

type formatType string

func (f *formatType) String() string {
    return fmt.Sprint(*f)
}

func (f *formatType) Set(value string) error {
    if len(*f) > 0 && *f != "text" {
        return errors.New("format flag already set")
    }
    if value != "text" && value != "json" && value != "hash" {
        return errors.New("Invalid Format Type")
    }
    *f = formatType(value)
    return nil
}

var typeFlag formatType

func init() {
    typeFlag = "text"
    usage := `Format type. Must be "text", "json" or "hash". Defaults to "text".`
    flag.Var(&typeFlag, "format", usage)
    flag.Var(&typeFlag, "f", usage+" (shorthand)")
}

func main() {
    flag.Parse()
    fmt.Println("Format type is", typeFlag)
}

对于这样一个简单的示例,这可能有点过头了,但在定义更复杂的标志类型时可能非常有用(链接的示例将逗号分隔的间隔列表转换为基于 的自定义类型的切片time.Duration)。

编辑:在回答如何针对标志运行单元测试时,最典型的例子是flag_test.go在标志包源中。与测试自定义标志变量相关的部分从第 181 行开始。

于 2013-07-02T10:36:17.550 回答
1

我不确定我们是否同意“单元测试”这个术语。在我看来,您想要实现的更像是程序中的一个非常正常的测试。你可能想做这样的事情:

func main() {
    flag.Parse()

    if formatType != text || formatType != json || formatType != hash {
        flag.Usage()
        return
    }

    // ...
}

可悲的是,用自己的值验证器扩展标志 Parser 并不容易,所以你现在必须坚持这一点。

有关定义自定义格式类型及其验证器的解决方案,请参阅 Intermernet。

于 2013-07-02T08:10:09.090 回答
1

你可以这样做

func main() {
    var name string
    var password string
    flag.StringVar(&name, "name", "", "")
    flag.StringVar(&password, "password", "", "")
    flag.Parse()
    for _, v := range os.Args {
        fmt.Println(v)
    }
    if len(strings.TrimSpace(name)) == 0 || len(strings.TrimSpace(password)) == 0 {
        log.Panicln("no name or no passward")
    }
    fmt.Printf("name:%s\n", name)
    fmt.Printf("password:%s\n", password)
}

func TestMainApp(t *testing.T) {
    os.Args = []string{"test", "-name", "Hello", "-password", "World"}
    main()
}

于 2021-08-30T03:38:17.463 回答
1

您可以main()通过以下方式进行测试:

  1. 进行运行命令的测试
  2. 然后go test直接调用由 构建的应用程序测试二进制文件
  3. 传递您要测试的所需标志
  4. 传回您可以断言的退出代码、标准输出和标准错误。

注意这仅在 main 退出时有效,因此测试不会无限运行,也不会陷入递归循环。

鉴于你的main.go样子:

package main

import (
    "flag"
    "fmt"
    "os"
)

var formatType string

const (
    text = "text"
    json = "json"
    hash = "hash"
)

func init() {
    const (
        defaultFormat = "text"
        formatUsage   = "desired output format"
    )

    flag.StringVar(&formatType, "format", defaultFormat, formatUsage)
    flag.StringVar(&formatType, "f", defaultFormat, formatUsage+" (shorthand)")
}

func main() {
    flag.Parse()
    fmt.Printf("format type = %v\n", formatType)
    os.Exit(0)
}

main_test.go可能看起来像:

package main

import (
    "fmt"
    "os"
    "os/exec"
    "path"
    "runtime"
    "strings"
    "testing"
)

// This will be used to pass args to app and keep the test framework from looping
const subCmdFlags = "FLAGS_FOR_MAIN"

func TestMain(m *testing.M) {
    // Only runs when this environment variable is set.
    if os.Getenv(subCmdFlags) != "" {
        runAppMain()
    }

    // Run all tests
    exitCode := m.Run()
    // Clean up
    os.Exit(exitCode)
}

func TestMainForCorrectness(tester *testing.T) {
    var tests = []struct {
        name     string
        wantCode int
        args     []string
    }{
        {"formatTypeJson", 0, []string{"-format", "json"}},
    }

    for _, test := range tests {
        tester.Run(test.name, func(t *testing.T) {
            cmd := getTestBinCmd(test.args)

            cmdOut, cmdErr := cmd.CombinedOutput()

            got := cmd.ProcessState.ExitCode()

            // Debug
            showCmdOutput(cmdOut, cmdErr)

            if got != test.wantCode {
                t.Errorf("unexpected error on exit. want %q, got %q", test.wantCode, got)
            }
        })
    }
}

// private helper methods.

// Used for running the application's main function from other test.
func runAppMain() {
    // the test framework has process its flags,
    // so now we can remove them and replace them with the flags we want to pass to main.
    // we are pulling them out of the environment var we set.
    args := strings.Split(os.Getenv(subCmdFlags), " ")
    os.Args = append([]string{os.Args[0]}, args...)

    // Debug stmt, can be removed
    fmt.Printf("\nos args = %v\n", os.Args)

    main() // will run and exit, signaling the test framework to stop and return the exit code.
}

// getTestBinCmd return a command to run your app (test) binary directly; `TestMain`, will be run automatically.
func getTestBinCmd(args []string) *exec.Cmd {
    // call the generated test binary directly
    // Have it the function runAppMain.
    cmd := exec.Command(os.Args[0], "-args", strings.Join(args, " "))
    // Run in the context of the source directory.
    _, filename, _, _ := runtime.Caller(0)
    cmd.Dir = path.Dir(filename)
    // Set an environment variable
    // 1. Only exist for the life of the test that calls this function.
    // 2. Passes arguments/flag to your app
    // 3. Lets TestMain know when to run the main function.
    subEnvVar := subCmdFlags + "=" + strings.Join(args, " ")
    cmd.Env = append(os.Environ(), subEnvVar)

    return cmd
}

func showCmdOutput(cmdOut []byte, cmdErr error) {
    if cmdOut != nil {
        fmt.Printf("\nBEGIN sub-command out:\n%v", string(cmdOut))
        fmt.Print("END sub-command\n")
    }

    if cmdErr != nil {
        fmt.Printf("\nBEGIN sub-command stderr:\n%v", cmdErr.Error())
        fmt.Print("END sub-command\n")
    }
}
于 2021-09-30T13:14:17.997 回答