如何在Go中对命令行标志进行单元测试?

huangapple go评论89阅读模式
英文:

How do I unit test command line flags in Go?

问题

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

以下是我想要编写测试的代码:

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等于上述常量值之一时,所需的测试才会通过。这个值将在formatType中可用。一个正确的调用示例是:program -format text

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

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

英文:

I would like a unit test that verifies a particular command line flag is within an enumeration.

Here is the code I would like to write tests against:

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()
}

The desired test would pass only if -format equalled one of the const values given above. This value would be available in formatType. An example correct call would be: program -format text

What is the best way to test the desired behaviors?

Note: Perhaps I have phrased this poorly, but the displayed code it not the unit test itself, but the code I want to write unit tests against. This is a simple example from the tool I am writing and wanted to ask if there were a good way to test valid inputs to the tool.

答案1

得分: 14

flag.Var函数可以在flag包中实现自定义测试和处理标志。

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

flag.Value是满足Value接口的任何类型,该接口定义如下:

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

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

对于您的用例,您可以使用类似以下的代码:

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包源代码中的flag_test.go。与测试自定义标志变量相关的部分从第181行开始。

英文:

Custom testing and processing of flags can be achieved with the flag.Var function in the flag package.

Flag.Var "defines a flag with the specified name and usage string. The type and value of the flag are represented by the first argument, of type Value, which typically holds a user-defined implementation of Value."

A flag.Value is any type that satisfies the Value interface, defined as:

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

There is a good example in the example_test.go file in the flag package source

For your use case you could use something like:

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)
}

This is probably overkill for such a simple example, but may be very useful when defining more complex flag types (The linked example converts a comma separated list of intervals into a slice of a custom type based on time.Duration).

EDIT: In answer to how to run unit tests against flags, the most canonical example is flag_test.go in the flag package source. The section related to testing custom flag variables starts at Line 181.

答案2

得分: 4

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()
}
英文:

You can do this

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()
}

答案3

得分: 2

您可以通过以下方式测试main()函数:

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

注意:这仅在主函数退出时才有效,以防止测试无限运行或陷入递归循环。

给定您的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"
)

// 这将用于传递参数给应用程序并防止测试框架进入循环
const subCmdFlags = "FLAGS_FOR_MAIN"

func TestMain(m *testing.M) {
    // 仅当设置了此环境变量时才运行
    if os.Getenv(subCmdFlags) != "" {
        runAppMain()
    }

    // 运行所有测试
    exitCode := m.Run()
    // 清理
    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()

            // 调试
            showCmdOutput(cmdOut, cmdErr)

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

// 私有辅助方法。

// 用于从其他测试中运行应用程序的主函数。
func runAppMain() {
    // 测试框架已处理其标志,
    // 现在我们可以删除它们并用我们想要传递给主函数的标志替换它们。
    // 我们从设置的环境变量中提取它们。
    args := strings.Split(os.Getenv(subCmdFlags), " ")
    os.Args = append([]string{os.Args[0]}, args...)

    // 调试语句,可以删除
    fmt.Printf("\nos args = %v\n", os.Args)

    main() // 运行并退出,表示测试框架停止并返回退出代码。
}

// getTestBinCmd返回一个命令,用于直接运行您的应用程序(测试)二进制文件;`TestMain`将自动运行。
func getTestBinCmd(args []string) *exec.Cmd {
    // 直接调用生成的测试二进制文件
    // 它有函数runAppMain。
    cmd := exec.Command(os.Args[0], "-args", strings.Join(args, " "))
    // 在源目录的上下文中运行。
    _, filename, _, _ := runtime.Caller(0)
    cmd.Dir = path.Dir(filename)
    // 设置环境变量
    // 1. 仅存在于调用此函数的测试的生命周期内。
    // 2. 传递参数/标志给您的应用程序
    // 3. 让TestMain知道何时运行主函数。
    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")
    }
}
英文:

You can test main() by:

  1. Making a test that runs a command
  2. Which then calls the app test binary, built from go test, directly
  3. Passing the desired flags you want to test
  4. Passing back the exit code, stdout, and stderr which you can assert on.

NOTE This only works when main exits, so that the test does not run infinitely, or gets caught in a recursive loop.

Given your main.go looks like:

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)
}

Your main_test.go may then look something like:

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")
    }
}

答案4

得分: 1

我不确定我们是否对“单元测试”一词达成一致。你想要实现的似乎更像是程序中的一个普通测试。你可能想要做类似这样的事情:

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

很遗憾,无法轻松地扩展flag Parser以使用自己的值验证器,所以你现在只能坚持这个。

请参考Intermernet的解决方案,该解决方案定义了自定义格式类型及其验证器。

英文:

I'm not sure whether we agree on the term 'unit test'. What you want to achieve seems to me
more like a pretty normal test in a program. You probably want to do something like this:

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

<strike>Sadly, it is not easily possible to extend the flag Parser with own value verifiers
so you have to stick with this for now.</strike>

See Intermernet for a solution which defines a custom format type and its validator.

huangapple
  • 本文由 发表于 2013年7月2日 04:17:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/17412908.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定