Go语言:利用 TDD 驱动开发测试 学习结构体、方法和接口
Perimeter(width float64, height float64)
float64
是形如 123.45 的浮点数。
先写测试函数
func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } }
这里的 f
对应 float64
,.2
表示输出 2 位小数。
运行测试
./shapes_test.go:6:9: undefined: Perimeter
为运行测试函数编写最少的代码并检查失败时的输出
func Perimeter(width float64, height float64) float64 { return 0 }
运行结果是:shapes_test.go:10: got 0 want 40
编写正确的代码让测试函数通过
func Perimeter(width float64, height float64) float64 { return 2*(width + height) }
func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func TestArea(t *testing.T) { got := Area(12.0, 6.0) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } }
相应的代码如下:
func Perimeter(width float64, height float64) float64 { return 2 * (width + height) } func Area(width float64, height float64) float64 { return width * height }
重构
type Rectangle struct { Width float64 Height float64 }
现在让我们用类型 Rectangle 代替简单的 float64 来重构这些测试函数。
func TestPerimeter(t *testing.T) { rectangle := Rectangle{10.0, 10.0} got := Perimeter(rectangle) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func TestArea(t *testing.T) { rectangle := Rectangle{12.0, 6.0} got := Area(rectangle) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } }
先运行这些测试函数再尝试修复问题,因为运行后我们能获得有用的错误信息:
./shapes_test.go:7:18: not enough arguments in call to Perimeter
have (Rectangle)
want (float64, float64)
我们可以通过下面的语法来访问一个 struct 中的域: myStruct.field
代码需要调整如下
func Perimeter(rectangle Rectangle) float64 { return 2 * (rectangle.Width + rectangle.Height) } func Area(rectangle Rectangle) float64 { return rectangle.Width * rectangle.Height }
先写测试函数
func TestArea(t *testing.T) { t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} got := Area(rectangle) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } }) t.Run("circles", func(t *testing.T) { circle := Circle{10} got := Area(circle) want := 314.16 if got != want { t.Errorf("got %.2f want %.2f", got, want) } }) }
运行测试
./shapes_test.go:28:13: undefined: Circle
为运行测试函数编写最少的代码并检查失败时的输出
我们需要定义一个 Circle 类型
type Circle struct { Radius float64 }
现在我们重新运行测试:
./shapes_test.go:29:14: cannot use circle (type Circle) as type Rectangle in argument to Area
有些编程语言中我们可以这样做:
func Area(circle Circle) float64 { ... } func Area(rectangle Rectangle) float64 { ... }
但是在 Go 语言中你不能这么做
./shapes.go:20:32: Area redeclared in this block
-
不同的包可以有函数名相同的函数。所以我们可以在一个新的包里创建函数 Area(Circle)。但是感觉有点大才小用了
- 我们可以为新类型定义方法
什么是方法?
func TestArea(t *testing.T) { t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} got := rectangle.Area() want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } }) t.Run("circles", func(t *testing.T) { circle := Circle{10} got := circle.Area() want := 314.1592653589793 if got != want { t.Errorf("got %f want %f", got, want) } }) }
尝试运行测试函数,我们会得到如下结果:
./shapes_test.go:19:19: rectangle.Area undefined (type Rectangle has no field or method Area)
./shapes_test.go:29:16: circle.Area undefined (type Circle has no field or method Area)
大家可以看到编译器的伟大之处。花些时间慢慢阅读这个错误信息是很重要的,这种习惯将对你长期有用。
为运行测试函数编写最少的代码并检查失败时的输出
我们给这些类型加一些方法:
type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area() float64 { return 0 } type Circle struct { Radius float64 } func (c Circle) Area() float64 { return 0 }
func(receiverName ReceiverType) MethodName(args)
。receiverName
获得。this
来获得接收者。r Rectangle
现在尝试重新运行测试,编译通过了但是会有一些错误输出。
编写足够的代码让测试函数通过
现在让我们修改我们的新方法以让矩形测试通过:
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
重构
Area()
方法并检查结果。CheckArea
,其参数是任何类型的几何形状。func TestArea(t *testing.T) { checkArea := func(t *testing.T, shape Shape, want float64) { t.Helper() got := shape.Area() if got != want { t.Errorf("got %.2f want %.2f", got, want) } } t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} checkArea(t, rectangle, 72.0) }) t.Run("circles", func(t *testing.T) { circle := Circle{10} checkArea(t, circle, 314.1592653589793) }) }
type Shape interface { Area() float64 }
Rectangle
和 Circle
一样创建了一个新类型,不过这次是 interface 而不是 struct。interface
的方式与大部分其他编程语言不同。通常接口定义需要这样的代码 My type Foo implements interface Bar
-
Rectangle
有一个返回值类型为float64
的方法Area
,所以它满足接口Shape
-
Circle
有一个返回值类型为float64
的方法Area
,所以它满足接口Shape
string
没有这种方法,所以它不满足这个接口
解耦
进一步重构
func TestArea(t *testing.T) { areaTests := []struct { shape Shape want float64 }{ {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, } for _, tt := range areaTests { got := tt.shape.Area() if got != tt.want { t.Errorf("got %.2f want %.2f", got, tt.want) } } }
先写测试函数
为我们的新类型添加测试用例非常容易,只需添加 "{Triangle{12,6},36.0}," 到我们的列表中去就行了。
func TestArea(t *testing.T) { areaTests := []struct { shape Shape want float64 }{ {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, {Triangle{12, 6}, 36.0}, } for _, tt := range areaTests { got := tt.shape.Area() if got != tt.want { t.Errorf("got %.2f want %.2f", got, tt.want) } } }
尝试运行测试函数
记住,不断尝试运行这些测试函数并让编译器引导你找到正确的方案
为运行测试函数编写最少的代码并检查失败时的输出
./shapes_test.go:25:4: undefined: Triangle
我们还没有定义 Triangle 类型:
type Triangle struct { Base float64 Height float64 }
再运行一次测试函数:
./shapes_test.go:25:8: cannot use Triangle literal (type Triangle) as type Shape in field value:
Triangle does not implement Shape (missing Area method)
编译器告诉我们不能把 Triangle 当作一个类型因为它没有方法 Area()。所以我们添加一个空的实现让测试函数能工作
func (c Triangle) Area() float64 { return 0 }
func (c Triangle) Area() float64 { return (c.Base * c.Height) * 0.5 }
最后测试通过了!
重构
{Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, {Triangle{12, 6}, 36.0},
{shape: Rectangle{Width: 12, Height: 6}, want: 72.0}, {shape: Circle{Radius: 10}, want: 314.1592653589793}, {shape: Triangle{Base: 12, Height: 6}, want: 36.0},
在 Kent Beck 的这篇题为 测试驱动开发实例 的帖子中把测试用例重构成要点和断言:
当测试用例不是一系列操作,而是事实的断言时,测试才清晰明了。
现在我们的测试用例是关于几何图形的面积这些事实的断言了。
确保测试输出有效
shapes_test.go:31: got 0.00 want 36.00
-------- FAIL: TestArea (0.00s)
--- FAIL: TestArea/Rectangle (0.00s)
shapes_test.go:33: main.Rectangle{Width:12, Height:6} got 72.00 want 72.10
我们可以通过如下命令来运行列表中指定的测试用例: go test -run TestArea/Rectangle
下面是满足要求的最终测试代码:
func TestArea(t *testing.T) { areaTests := []struct { name string shape Shape hasArea float64 }{ {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0}, {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793}, {name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0}, } for _, tt := range areaTests { // using tt.name from the case to use it as the `t.Run` test name t.Run(tt.name, func(t *testing.T) { got := tt.shape.Area() if got != tt.hasArea { t.Errorf("%v got %.2f want %.2f", tt.shape, got, tt.hasArea) } }) } }
总结
这是进一步的 TDD 实践。我们在对一个基本数学问题的解决方案的迭代中,通过测试学习了语言的新特性。
-
声明结构体以创建我们自己的类型,让我们把数据集合在一起并达到简化代码的目地
-
声明接口,这样我们可以定义适合不同参数类型的函数(参数多态)
-
在自己的数据类型中添加方法以实现接口
- 列表驱动测试让断言更清晰,这样可以使测试文件更易于扩展和维护
本文来自博客园,作者:slowlydance2me,转载请注明原文链接:https://www.cnblogs.com/slowlydance2me/p/17237233.html