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 BarRectangle有一个返回值类型为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 实践。我们在对一个基本数学问题的解决方案的迭代中,通过测试学习了语言的新特性。
- 声明结构体以创建我们自己的类型,让我们把数据集合在一起并达到简化代码的目地
- 声明接口,这样我们可以定义适合不同参数类型的函数(参数多态)
- 在自己的数据类型中添加方法以实现接口
- 列表驱动测试让断言更清晰,这样可以使测试文件更易于扩展和维护
Go语言:利用 TDD 驱动开发测试 学习结构体、方法和接口的更多相关文章
- C语言开发函数库时利用不透明指针对外隐藏结构体细节
1 模块化设计要求库接口隐藏实现细节 作为一个函数库来说,尽力降低和其调用方的耦合.是最主要的设计标准. C语言,作为经典"程序=数据结构+算法"的践行者,在实现函数库的时候,必定 ...
- C语言中两个相同类型的结构体变量之间是可以相互直接赋值的
C语言中,在相同类型的变量间赋值时是直接内存复制的,即将他们的内存进行复制,而两个同类型的结构体变量属于同一种变量,所以赋值时是按照他们的内存分布来直接拷贝的.所以,在C语言中两个相同类型的结构体变量 ...
- Windows内核驱动开发入门学习资料
声明:本文所描述的所有资料和源码均搜集自互联网,版权归原始作者所有,所以在引用资料时我尽量注明原始作者和出处:本文所搜集资料也仅供同学们学习之用,由于用作其他用途引起的责任纠纷,本人不负任何责任.(本 ...
- ios开发中的C语言学习—— 结构体简介
在开发过程中,经常会需要处理一组不同类型的数据,比如学生的个人信息,由姓名.年龄.性别.身高等组成,因为这些数据是由不同数据类型组成的,因此不能用数组表示,对于不同数据类型的一组数据,可以采用结构体来 ...
- go语言学习-结构体
结构体 go语言中的结构体,是一种复合类型,有一组属性构成,这些属性被称为字段.结构体也是值类型,可以使用new来创建. 定义: type name struct { field1 type1 fie ...
- c语言学生信息管理系统-学习结构体
#include<stdio.h> #include<stdlib.h> //结构体可以存放的学生信息最大个数,不可变变量 ; //学生信息结构体数组,最多可以存放100个学生 ...
- 29个android开发常用的类、方法及接口
在安卓开发中,我们常常都需要借助各种各样的方法.类和接口来实现相关功能.提升开发效率,但对于初学者而言,什么时候该用什么类.方法和接口呢?下面小编整理了29个,日常开发中比较常用的类.方法.接口及其应 ...
- Go语言 - 结构体 | 方法
自定义类型和类型别名 自定义类型 在Go语言中有一些基本的数据类型,如string.整型.浮点型.布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型. 自定义类型是定义了一个全新的类型 ...
- 全国计算机等级考试二级教程-C语言程序设计_第14章_结构体、共用体和用户定义类型
函数的返回值是结构体类型 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> struct ...
- 网络驱动移植之net_device结构体及其相关的操作函数
内核源码:Linux-2.6.38.8.tar.bz2 在Linux系统中,网络设备都被抽象为struct net_device结构体.它是网络设备硬件与上层协议之间联系的接口,了解它对编写网络驱动程 ...
随机推荐
- 记一次前端ajax禁止使用异步async的操作
环境: 前端layui jquery 情况: 页面在iframe里面, 然后点击按钮,弹出输入框.点击确认,弹出框发送内容到后台, 传送数据到后台后,然后根据返回一个map给前端.前端解析数据,返回 ...
- python爬虫实战——自动下载百度图片(文末附源码)
用Python制作一个下载图片神器 前言 这个想法是怎么来的? 很简单,就是不想一张一张的下载图片,嫌太慢. 在很久很久以前,我比较喜欢收集各种动漫的壁纸,作为一个漫迷,自然是能收集多少就收集多少.小 ...
- sdio/mmc/sd笔记
[SDIO] SD card 初始化及常用命令解析 https://blog.csdn.net/u010443710/article/details/107014873 cmd0命令,是单向命令,ho ...
- vue中关于get传参数为数组的解决方法
按理来说,get请求方式是没有数组的,get请求方式带参数都是字符串,需要和后端协商是用某个标识符分割开,例如"|" ",". 当然如果需要数组的话,也能解 ...
- 关闭Google自动更新
一.禁用任务计划 二.禁用更新服务 三.重命名更新程序 首先找到谷歌浏览器的安装位置
- DEM高程数据下载资源
最近发现了几个比较好的DEM高程数据免费下载资源,遂总结一下. clouldRF(https://cloudrf.com/terrain%20data)官方网站有说明其支持的地形数据来源,主要包括如下 ...
- 简单了解promise
promise是什么: JavaScript中存在很多异步操作, Promise将异步操作队列化,按照期望的顺序执行,返回 符合预期的结果.可以通过链式调用多个 Promise达到我们的目的. Pro ...
- Sql Server新建一个只读权限的用户
1,新建只能访问某一个表的只读用户. --添加只允许访问指定表的用户: exec sp_addlogin '用户名','密码','默认数据库名' --添加到数据库 exec sp_grantdbacc ...
- Web _Servlet(url-pattern)的配置与优先级
url-pattern的配置方式有三种: 1.完全路径匹配:以 '/' 开始 例: /ServletDemo1 , /aaa/ServletDemo2 , /aa/bb/ServletDemo3 ...
- FCC 高级算法题 库存更新
Inventory Update 依照一个存着新进货物的二维数组,更新存着现有库存(在 arr1 中)的二维数组. 如果货物已存在则更新数量 . 如果没有对应货物则把其加入到数组中,更新最新的数量. ...