繁体   English   中英

在 Golang 中测试包装函数的惯用方法是什么?

[英]What is the idiomatic way to test wrapper functions in Golang?

这并不罕见,当我们有一个带有很多参数的 function 时,创建一些使用带有预定义参数的通用 function 的伴随函数。 这是定义参数默认值的通常替代方法,Go 不允许这样做。

一个非常简单的示例是创建一个SquareArea function,它使用更通用的RectangleArea function。

func SquareArea(side int) int {
    return RectangleArea(side, side)
}

func RectangleArea(width int, length int) int {
    if width < 0 || length < 0 {
        return 0
    }
    return width * length
}

对于这些功能,可以创建(有点)表测试

func TestSquareArea(t *testing.T) {
    tests := []struct {
        side     int
        expected int
    }{
        {side: -1, expected: 0},
        {side: 0, expected: 0},
        {side: 1, expected: 1},
        {side: 2, expected: 4},
        {side: 100, expected: 10000},
    }
    for idx, test := range tests {
        if result := SquareArea(test.side); test.expected != result {
            t.Errorf("test #%d expected != result (%d!=%d)", idx, test.expected, result)
        }
    }
}

func TestRectangleArea(t *testing.T) {
    tests := []struct {
        width    int
        length   int
        expected int
    }{
        {width: -1, length: -1, expected: 0},
        {width: -1, length: 1, expected: 0},
        {width: 1, length: -1, expected: 0},
        {width: 2, length: 1, expected: 2},
        {width: 1, length: 2, expected: 2},
        {width: 3, length: 2, expected: 6},
        {width: 2, length: 3, expected: 6},
        {width: 10, length: 100, expected: 1000},
        {width: 100, length: 10, expected: 1000},
    }
    for idx, test := range tests {
        if result := RectangleArea(test.width, test.length); test.expected != result {
            t.Errorf("test #%d expected != result (%d!=%d)", idx, test.expected, result)
        }
    }
}

这是正确的方法吗?

对于更复杂的功能和更多的配套功能,这种方法似乎会创建大量测试,同时实质上会多次测试相同的底层代码(即RectangleArea )。

我觉得测试SquareArea应该只确保使用正确的参数调用RectangleArea

这种代码方法(同时具有RectangleAreaSquareArea功能)在 Golang 中是惯用的吗? 在 Golang 上还有其他方法吗?

使用像 Python 这样的语言,我会模拟RectangleArea function。

为了模仿这一点,我会做类似的事情:

var rectangleAreaFunction = RectangleArea

func SquareArea(side int) int {
    return rectangleAreaFunction(side, side)
}

func RectangleArea(width int, length int) int {
    if width < 0 || length < 0 {
        return 0
    }
    return width * length
}

通过测试:

func TestSquareArea(t *testing.T) {
    call:= ""
    mock:= func(width int, length int) int {
        call = fmt.Sprintf("func(%d,%d)", width, length)
        return 0
    }
    rectangleAreaFunction = mock
    tests := []struct {
        side     int
        expected string
    }{
        {side: -1, expected: "func(-1,-1)"},
        {side: 0, expected: "func(0,0)"},
        {side: 1, expected: "func(1,1)"},
        {side: 2, expected: "func(2,2)"},
        {side: 100, expected: "func(100,100)"},
    }
    for idx, test := range tests {
        call = ""
        SquareArea(test.side)
        if test.expected != call {
            t.Errorf("test #%d expected != call (%s!=%s)", idx, test.expected, call)
        }
    }
}


func TestRectangleArea(t *testing.T) {
    tests := []struct {
        width    int
        length   int
        expected int
    }{
        {width: -1, length: -1, expected: 0},
        {width: -1, length: 1, expected: 0},
        {width: 1, length: -1, expected: 0},
        {width: 2, length: 1, expected: 2},
        {width: 1, length: 2, expected: 2},
        {width: 3, length: 2, expected: 6},
        {width: 2, length: 3, expected: 6},
        {width: 10, length: 100, expected: 1000},
        {width: 100, length: 10, expected: 1000},
    }
    for idx, test := range tests {
        if result := RectangleArea(test.width, test.length); test.expected != result {
            t.Errorf("test #%d expected != result (%d!=%d)", idx, test.expected, result)
        }
    }
}

但是似乎使用变量来保存实际的 function 来进行测试是不好的。

这种情况的另一个例子是通过第一次分类 function 处理 http 请求方法时:

func Triage(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    // some other common code like adding some context
    *r = *(r.WithContext(context.WithValue(r.Context(), "key", "value")))
    switch r.Method {
    case POST:
        triagePost(w, r)
    default:
        triageGet(w, r)
    }
}

func triagePost(w http.ResponseWriter, r *http.Request) {
    // some code
}

func triageGet(w http.ResponseWriter, r *http.Request) {
    // some code
}

对这 3 个功能的完整测试将有很多无用的重复将Triage function 的测试限制为:

  • 检查公共代码是否正确执行
  • 检查是否根据r.Method正确调用了正确的 function( triagePosttriageGet

将在测试实际行为时防止此类重复。

或者在 Golang 中拥有这种类型的代码(分类功能)是完全不正确的?

Go尽量避免mocking。 让我们尝试遵循惯用的方法(测试真实代码),看看它能带给我们多远。

Function SquareArea 使用相同的两个参数值调用 RectangleArea。 测试 SquareArea 并不关心 RectangleArea 的正确性。

SquareArea 的单个测试用例将提供 100% 的测试覆盖率,甚至不需要表驱动测试。

如果担心在给定两个相同值的参数时测试 RectangleArea 的正确性,则应将此测试用例直接添加到 RectangleArea 的表驱动测试中。

当然,如果 SquareArea 更复杂,那么我们将需要一个表驱动测试。 但在这种情况下,我们也必须关注 SquareArea 的作用,而不是RectangleArea 的作用。

换句话说,在决定如何测试 function(或任何实际的东西)时,我发现以下方法有助于减少疑虑:

  1. 不断重复自己:我到底在测试什么???
  2. 使用代码覆盖来指导我。 代码覆盖率(并固定在百分比上)可能是一把双刃剑,但明智地用作指导会非常有用。
  3. 挑战测试的存在。 测试的问题在于它们的存在往往证明被测试代码的存在是合理的。 如果测试的代码没有在其他地方使用,删除它和它的测试。 根据我的经验,这是 TDD 最有价值的一课(如果适用)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM