如何在golang中测试主要的包功能?

Posted

技术标签:

【中文标题】如何在golang中测试主要的包功能?【英文标题】:How to test the main package functions in golang? 【发布时间】:2015-09-29 21:07:07 【问题描述】:

我想测试我的主包中包含的一些函数,但我的测试似乎无法访问这些函数。

我的示例 main.go 文件如下所示:

package main

import (
    "log"
)

func main() 
    log.Printf(foo())


func foo() string 
    return "Foo"

我的 main_test.go 文件看起来像:

package main

import (
    "testing"
)

func Foo(t testing.T) 
    t.Error(foo())

当我运行 go test main_test.go 时,我得到了

# command-line-arguments
.\main_test.go:8: undefined: foo
FAIL    command-line-arguments [build failed]

据我了解,即使我将测试文件移到别处并尝试从 main.go 文件导入,我也无法导入它,因为它是 package main

构建此类测试的正确方法是什么?我应该从main 包中删除所有内容,并使用一个简单的主函数来运行所有内容,然后在它们自己的包中测试这些函数,还是有办法在测试期间从主文件中调用这些函数?

【问题讨论】:

本视频有很好的测试包介绍视频(3m30s开始)https://youtu.be/XCsL89YtqCs?t=3m30s 您的 main() 函数(理想情况下,您的包 main 作为一个整体)不需要测试:它应该是一个或多个库的“哑”端点。测试那些。 main.go 中的函数“foo”以小写“f”开头,使其私有,所以我认为您的测试不能直接调用它,因为它没有访问权限。跨度> 【参考方案1】:

当你在命令行指定文件时,你必须指定所有文件

这是我的跑步:

$ ls
main.go     main_test.go
$ go test *.go
ok      command-line-arguments  0.003s

注意,在我的版本中,我在命令行上同时使用 main.go 和 main_test.go 运行

另外,你的 _test 文件不太对,你需要调用你的测试函数 TestXXX 并带一个指向测试的指针。T

这是修改后的版本:

package main

import (
    "testing"
)

func TestFoo(t *testing.T) 
    t.Error(foo())

以及修改后的输出:

$ go test *.go
--- FAIL: TestFoo (0.00s)
    main_test.go:8: Foo
FAIL
FAIL    command-line-arguments  0.003s

【讨论】:

你为什么要指定任何文件到go test?不要。 true,但我认为角色要求有点挣扎,我不想将问题与设置 GOPATH 或类似的东西混淆。做“去测试”。会工作? (之前没有在 GOPATH 之外尝试过) 您不希望您的测试文件位于主包中。如果你这样做,你最终会得到很多你不想要的代码和标志。【参考方案2】:

单元测试仅此而已。在某些时候,您必须实际运行该程序。然后你测试它是否适用于来自真实来源的真实输入,产生真实输出到真实目的地。真的。

如果你想对某个事物进行单元测试,请将其移出 main()。

【讨论】:

但是要达到100%的覆盖率,需要对main函数进行测试,这个怎么实现呢? @perrohunter 100% 的测试覆盖率是海市蜃楼。不要打扰它。我最讨厌不切实际的测试覆盖率策略的事情是,有一次我在文件关闭之类的东西上添加了一个 if err,它降低了测试覆盖率,因为我们无法模拟文件关闭错误。它可能未经测试,但这比忽略错误并假装它不会发生要好 100%! 我不想在这里引发一场代码覆盖率的激战,但我不同意 100% 覆盖率是海市蜃楼的说法。务实地说,是的,有时由于满足覆盖政策而延迟交付软件的成本超过了不覆盖手动测试代码的成本。但是当你说你不能模拟文件关闭错误时,这实际上是一个设计缺陷。这就是接口在 Go 中派上用场的地方。例如,“func Foo(file os.File)”不像“func Foo(file io.ReadWriteCloser)”那样容易测试 调用 main() 来启动您的 Web 应用程序以运行集成测试是“测试 main”的正当理由,如果您使用 Cucumber 来测试行为也很有用。【参考方案3】:

不是对 OP 问题的直接回答,我基本同意先前的回答,并且 cmets 敦促 main 应该主要是打包函数的调用者。话虽如此,我发现这是一种对测试可执行文件有用的方法。它利用了log.Fatalnexec.Command

    使用延迟函数编写main.go,该函数调用 log.Fatalln() 以在返回之前将消息写入 stderr。 在main_test.go 中,使用exec.Command(...)cmd.CombinedOutput() 运行您的程序,并选择用于测试某些预期结果的参数。

例如:

func main() 
    // Ensure we exit with an error code and log message
    // when needed after deferred cleanups have run.
    // Credit: https://medium.com/@matryer/golang-advent-calendar-day-three-fatally-exiting-a-command-line-tool-with-grace-874befeb64a4
    var err error
    defer func() 
        if err != nil 
            log.Fatalln(err)
        
    ()

    // Initialize and do stuff

    // check for errors in the usual way
    err = somefunc()
    if err != nil 
        err = fmt.Errorf("somefunc failed : %v", err)
        return
    

    // do more stuff ...

 

main_test.go 中,测试应该导致somefunc 失败的错误参数可能如下所示:

func TestBadArgs(t *testing.T) 
    var err error
    cmd := exec.Command(yourprogname, "some", "bad", "args")
    out, err := cmd.CombinedOutput()
    sout := string(out) // because out is []byte
    if err != nil && !strings.Contains(sout, "somefunc failed") 
        fmt.Println(sout) // so we can see the full output 
        t.Errorf("%v", err)
    

请注意,CombinedOutput() 中的 err 是 log.Fatalln 对 os.Exit(1) 的底层调用的非零退出代码。这就是为什么我们需要使用out 来从somefunc 中提取错误信息。

exec 包还提供了cmd.Runcmd.Output。对于某些测试,这些可能比cmd.CombinedOutput 更合适。我还发现有一个 TestMain(m *testing.M) 函数在运行测试之前和之后进行设置和清理很有用。

func TestMain(m *testing.M) 
    // call flag.Parse() here if TestMain uses flags
    os.Mkdir("test", 0777) // set up a temporary dir for generate files

    // Create whatever testfiles are needed in test/

    // Run all tests and clean up
    exitcode := m.Run()
    os.RemoveAll("test") // remove the directory and its contents.
    os.Exit(exitcode)

【讨论】:

【参考方案4】:

如何使用标志测试main 并声明退出代码

@MikeElis 的回答让我成功了一半,但缺少一个主要部分,Go 自己的 flag_test.go 帮助我弄清楚。

免责声明

您本质上是想运行您的应用并测试正确性。因此,请随意标记此测试并将其归档在该类别中。但值得尝试这种类型的测试并看到好处。特别是如果您正在编写 CLI 应用程序。

这个想法是像往常一样运行go test,并且

    使用go test 制作的应用程序的测试版本在子流程中“自行”运行单元测试(参见86 行) 我们还将环境变量(参见88 行)传递给子进程,该子进程将执行将运行main 的代码部分,并导致测试以main 的退出代码退出:
    if os.Getenv(SubCmdFlags) != "" 
        // We're in the test binary, so test flags are set, lets reset it so
        // so that only the program is set
        // and whatever flags we want.
        args := strings.Split(os.Getenv(SubCmdFlags), " ")
        os.Args = append([]stringos.Args[0], args...)
    
        // Anything you print here will be passed back to the cmd.Stderr and
        // cmd.Stdout below, for example:
        fmt.Printf("os args = %v\n", os.Args)
    
        // Strange, I was expecting a need to manually call the code in
        // `init()`,but that seem to happen automatically. So yet more I have learn.
        main()
    
    
    注意:如果 main 函数没有退出,测试将挂起/循环。 然后断言从子进程返回的退出代码。
    // get exit code.
    got := cmd.ProcessState.ExitCode()
    if got != test.want 
        t.Errorf("got %q, want %q", got, test.want)
    
    
    注意:在此示例中,如果返回的不是预期的退出代码,则测试会从子进程输出 STDOUT 和/或 STDERR,以获取调试帮助。

在此处查看完整示例:go-gitter: Testing the CLI

【讨论】:

【参考方案5】:

因为你只设置了一个文件进行测试,所以不会用到其他的go文件。

运行go test 而不是go test main_test.go

还将测试函数签名 Foo(t testing.T) 更改为 TestFoo(t *testing.T)

【讨论】:

【参考方案6】:

在两个源中将包名称从 main 更改为 foobar。 移动 src/foobar 下的源文件。

mkdir -p src/foobar
mv main.go main_test.go src/foobar/

确保将 GOPATH 设置为 src/foobar 所在的文件夹。

export GOPATH=`pwd -P`

测试一下

go test foobar

【讨论】:

答案并没有真正回答问题。这只是一个复杂的等价于在不指定文件的情况下调用go test

以上是关于如何在golang中测试主要的包功能?的主要内容,如果未能解决你的问题,请参考以下文章

golang:如何对私有函数进行单元测试

golang:如何对私有函数进行单元测试

如何使用一种功能测试我的包?

如何在 golang 中为我的测试用例正确创建模拟 http.request?

一看就懂系列之Golang的测试

在golang中进行集成测试时如何模拟外部http请求api