只在测试文件中隐藏包名

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

Mask package name in test file only

问题

为了实现100%的单元测试覆盖率,我们正在尝试测试一个函数中的几行代码。该函数调用了运行时包中的相关函数:

// functionName返回一个字符串,表示调用者上方n个堆栈帧的函数名。
// 如果n = 0,则返回调用functionName()的函数的名称。
func functionName(n int) string {
    pc, _, _, ok := runtime.Caller(n + 1)
    if !ok {
        return "unknown function"
    }
    me := runtime.FuncForPC(pc)
    if me == nil {
        return "unknown function"
    }

    split := strings.Split(me.Name(), ".")
    if len(split) == 0 {
        return "unknown function"
    }
    return split[len(split)-1]
}

具体来说,目前尚未测试的是三个if语句及其返回值,因为运行时函数似乎不容易操纵以返回我们想要的值。在这种情况下,我们通常的做法是模拟出相关项,但这些调用是对运行时包本身的包级函数(而不是接口的方法)。

我最初的想法是通过使用一个具有Caller()FuncForPC()方法的结构体来模拟运行时标记本身,并将其赋值给测试文件中的名为runtime的变量(这样就不会影响生产代码流程,因为测试文件在正常构建过程中被省略)。然而,这会触发一个关于在(全局)块中重新声明runtime的构建错误。

我知道如果runtime变量在非全局范围内声明(例如掩盖fmt),这是可能的,但我找不到一种优雅的方法来做到这一点,既可以在测试中屏蔽它,又不会在生产代码本身中屏蔽它。我想到的唯一方法是修改生产代码的源代码来声明这样一个变量,并在测试中替换它的值,但这远非理想,因为它仅仅为了测试目的而使生产代码变得复杂。

有什么想法吗?

英文:

In pursuit of 100% unit test coverage, we have several lines we're trying to test in one of our functions. The relevant function calls out to the runtime package:

// functionName returns a string representing the function name of the function n stack frames above the caller.
// if n = 0, the name of the function calling functionName() will be returned.
func functionName(n int) string {
	pc, _, _, ok := runtime.Caller(n + 1)
	if !ok {
		return "unknown function"
	}
	me := runtime.FuncForPC(pc)
	if me == nil {
		return "unknown function"
	}

	split := strings.Split(me.Name(), ".")
	if len(split) == 0 {
		return "unknown function"
	}
	return split[len(split)-1]
}

Specifically, the 3 if statements and their return values are currently untested, because the runtime functions don't appear to be easily manipulated to return the values we want. Our standard response in these cases is to mock out the items in question, but these calls are to package-level functions (rather than methods of an interface) within the runtime package itself.

My first thought was to mock out the runtime token itself by using a structure with Caller() and FuncForPC() methods, assigned to a variable named "runtime" in the test files (so it wouldn't affect the production code flow, since test files are omitted during normal builds). However, this triggers a build error about "runtime" being redeclared within the (global) block.

I know this would be possible if the "runtime" variable were declare in a non-global scope (example masking fmt), but I can't find an elegant way to do so such that it gets masked within the tests, but not within the production code itself. The only way I've thought of is by altering the source of the production code to declare such a variable and replacing it's value in the tests, but this is far from ideal, since it complicates the production code purely for the purposes of testing.

Any ideas?

答案1

得分: 2

一种解决方法是声明你想要模拟的那些函数的变量。

var runtimeCaller = runtime.Caller
var runtimeFuncForPC = runtime.FuncForPC

func functionName(n int) string {
    pc, _, _, ok := runtimeCaller(n + 1)
    if !ok {
        return "unknown function"
    }
    me := runtimeFuncForPC(pc)
    if me == nil {
        return "unknown function"
    }

    split := strings.Split(me.Name(), ".")
    if len(split) == 0 {
        return "unknown function"
    }
    return split[len(split)-1]
}

或者,如果你更喜欢点号表示法...

var _runtime = struct{
    Caller    func(skip int) (pc uintptr, file string, line int, ok bool)
    FuncForPC func(pc uintptr) *runtime.Func
}{runtime.Caller, runtime.FuncForPC}

func functionName(n int) string {
    pc, _, _, ok := _runtime.Caller(n + 1)
    if !ok {
        return "unknown function"
    }
    me := _runtime.FuncForPC(pc)
    if me == nil {
        return "unknown function"
    }

    split := strings.Split(me.Name(), ".")
    if len(split) == 0 {
        return "unknown function"
    }
    return split[len(split)-1]
}

在你的测试中,在运行functionName之前,你可以设置变量/字段来模拟实现。如果其他测试可能导致调用functionName,请注意并发访问...我认为在不显著改变现有代码的情况下,没有太多其他的事情可以做。

英文:

One solution is to declare variables of those functions you want to mock.

var runtimeCaller = runtime.Caller
var runtimeFuncForPC = runtime.FuncForPC

func functionName(n int) string {
    pc, _, _, ok := runtimeCaller(n + 1)
    if !ok {
        return "unknown function"
    }
    me := runtimeFuncForPC(pc)
    if me == nil {
        return "unknown function"
    }

    split := strings.Split(me.Name(), ".")
    if len(split) == 0 {
        return "unknown function"
    }
    return split[len(split)-1]
}

Or if you prefer the dot notation...

var _runtime = struct{
    Caller    func(skip int) (pc uintptr, file string, line int, ok bool)
    FuncForPC func(pc uintptr) *runtime.Func
}{runtime.Caller, runtime.FuncForPC}

func functionName(n int) string {
    pc, _, _, ok := _runtime.Caller(n + 1)
    if !ok {
        return "unknown function"
    }
    me := _runtime.FuncForPC(pc)
    if me == nil {
        return "unknown function"
    }

    split := strings.Split(me.Name(), ".")
    if len(split) == 0 {
        return "unknown function"
    }
    return split[len(split)-1]
}

And in your tests, before running functionName, you can set the variables/fields to mock implementations. And if other tests may cause the functionName to be called beware of concurrent access... I don't think there is much else you can do without changing the existing code significantly.

答案2

得分: 0

关于程序的可靠性

这个故事的第一个教训是,程序测试可以非常有效地用来显示错误的存在,但永远无法证明错误的不存在。

让我们来看看你的代码。Go中的int是一个32位或64位的有符号整数。因此,考虑以下代码:

funcname.go:

package main

import (
	"fmt"
	"runtime"
	"strings"
)

// functionName返回一个字符串,表示调用者上方n个堆栈帧的函数名。
// 如果n = 0,则返回调用functionName()的函数的名称。
func functionName(n int) string {
	pc, _, _, ok := runtime.Caller(n + 1)
	if !ok {
		return "unknown function"
	}
	me := runtime.FuncForPC(pc)
	if me == nil {
		return "unknown function"
	}

	split := strings.Split(me.Name(), ".")
	if len(split) == 0 {
		return "unknown function"
	}
	return split[len(split)-1]
}

func main() {
	for skip := -4; skip <= 4; skip++ {
		fn := functionName(skip)
		fmt.Println(functionName(0), skip, fn)
	}
	const (
		sizeInt = 32 << (^uint(0) >> 63)
		maxInt  = 1<<(sizeInt-1) - 1
		minInt  = -1 << (sizeInt - 1)
	)
	for _, skip := range []int{minInt, maxInt} {
		fn := functionName(skip)
		fmt.Println(functionName(0), skip, fn)
	}
}

输出结果:

$ go run funcname.go
main -4 skipPleaseUseCallersFrames
main -3 skipPleaseUseCallersFrames
main -2 skipPleaseUseCallersFrames
main -1 functionName
main 0 main
main 1 main
main 2 goexit
main 3 unknown function
main 4 unknown function
main -9223372036854775808 skipPleaseUseCallersFrames
main 9223372036854775807 skipPleaseUseCallersFrames
$  

在我看来,`functionName`中有一个bug。你的覆盖测试对此有何说法?

根据你的代码,似乎没有可靠的方法来检测错误值的返回。一种方法是返回一个空字符串。如果你想使用特殊值,比如`"unknown function"`,那么可以提供一个值进行检查。例如:

```go
const functionUnknown = "unknown function"

func functionName(n int) string {
	pc, file, line, ok := runtime.Caller(n + 1)
	if !ok {
		return functionUnknown
	}
    // . . .
}

func main() {
	fn := functionName(0)
    if fn == functionUnknown {
        // 处理错误
    }
}

你的覆盖测试对此有何说法?

英文:

> On the reliability of programs. Edsger W. Dijkstra
>
> The first moral of the story is that program testing can be used very
> effectively to show the presence of bugs but never to show their
> absence.


Let's read your code. Go type int is a 32- or 64-bit signed integer. Therefore, consider,

funcname.go:

package main

import (
	&quot;fmt&quot;
	&quot;runtime&quot;
	&quot;strings&quot;
)

// functionName returns a string representing the function name of the function n stack frames above the caller.
// if n = 0, the name of the function calling functionName() will be returned.
func functionName(n int) string {
	pc, _, _, ok := runtime.Caller(n + 1)
	if !ok {
		return &quot;unknown function&quot;
	}
	me := runtime.FuncForPC(pc)
	if me == nil {
		return &quot;unknown function&quot;
	}

	split := strings.Split(me.Name(), &quot;.&quot;)
	if len(split) == 0 {
		return &quot;unknown function&quot;
	}
	return split[len(split)-1]
}

func main() {
	for skip := -4; skip &lt;= 4; skip++ {
		fn := functionName(skip)
		fmt.Println(functionName(0), skip, fn)
	}
	const (
		sizeInt = 32 &lt;&lt; (^uint(0) &gt;&gt; 63)
		maxInt  = 1&lt;&lt;(sizeInt-1) - 1
		minInt  = -1 &lt;&lt; (sizeInt - 1)
	)
	for _, skip := range []int{minInt, maxInt} {
		fn := functionName(skip)
		fmt.Println(functionName(0), skip, fn)
	}
}

Output:

$ go run funcname.go
main -4 skipPleaseUseCallersFrames
main -3 skipPleaseUseCallersFrames
main -2 skipPleaseUseCallersFrames
main -1 functionName
main 0 main
main 1 main
main 2 goexit
main 3 unknown function
main 4 unknown function
main -9223372036854775808 skipPleaseUseCallersFrames
main 9223372036854775807 skipPleaseUseCallersFrames
$  

It looks like a bug in functionName to me. What did your coverage testing say about this?


Reading your code there appears to be no reliable way to detect the return of an error value. One way would be to return an empty string. If you want to use a special value such as &quot;unknown function&quot; then provide a value to check against. For example,

const functionUnknown = &quot;unknown function&quot;

func functionName(n int) string {
	pc, file, line, ok := runtime.Caller(n + 1)
	if !ok {
		return functionUnknown
	}
    // . . .
}

func main() {
	fn := functionName(0)
    if fn == functionUnknown {
        // handle error
    }
}

What did your coverage testing say about this?

huangapple
  • 本文由 发表于 2017年4月26日 03:59:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/43619913.html
匿名

发表评论

匿名网友

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

确定