环境安装:
上一篇相关随笔:
 
正文
先来点简单的:
假设我们需要编程计算一个给定高和宽的长方形的周长。我们可以写一个函数如下:
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)
}
到目前为止还很简单。现在让我们来编写一个函数 Area(width, height float64) 来返回长方形的面积。
你可以先自己按照 TDD 的方式尝试一下。
相应的测试函数如下所示:

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
}

重构

我们的代码能正常工作,但是其中不包含任何显式的信息表示计算的是长方形。
粗心的开发者可能会错误的调用这些函数来计算三角形的周长和面积而没有意识到错误的结果。
我们可以仅仅给这些函数命名成像 RectangleArea 一样更具体的名字。
但是更简洁的方案是定义我们自己的类型 Rectangle,它可以封装长方形的信息。
我们可以使用保留字 struct 来定义自己的类型。
一个通过 struct 定义出来的类型是一些已命名的域的集合,这些域用来保存数据。
 
 一个 struct 的声明如下:

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
}
通过传递一个类型为 Rectangle 的参数给这些函数更能表达我们的用意。

先写测试函数

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)。但是感觉有点大才小用了
 
  • 我们可以为新类型定义方法

什么是方法?

到目前为止我们只编写过函数但是我们已经使用过方法。
当我们调用 t.Errorf 时我们调用了 t(testing.T) 这个实例的方法 ErrorF。
方法和函数很相似但是方法是通过一个特定类型的实例调用的。
函数可以随时被调用,比如 Area(rectangle)。不像方法需要在某个事物上调用。
 示例会帮助我们理解。让我们通过方法调用的方式来改写测试函数并尝试修复代码。

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 来获得接收者。
 把类型的第一个字母作为接收者变量是 Go 语言的一个惯例。
r Rectangle

现在尝试重新运行测试,编译通过了但是会有一些错误输出。

编写足够的代码让测试函数通过

现在让我们修改我们的新方法以让矩形测试通过:

func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
现在重跑测试,矩形测试应该通过了但是圆的测试还是失败的。
 为使 Circle 测试通过我们需要从 math 包中借用常数 Pi(记得引入 math 包)。

func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

重构

我们的测试有些重复。
我们想做的是给定一些几何形状,调用 Area() 方法并检查结果。
我们想写一个这样的函数 CheckArea,其参数是任何类型的几何形状。
如果参数不是几何形状的类型,那么编译应该报错。 Go 语言中我们可以通过接口实现这一目的。
接口在 Go 这种静态类型语言中是一种非常强有力的概念。因为接口可以让函数接受不同类型的参数并能创造类型安全且高解耦的代码。
 让我们引入接口来重构我们的测试代码:

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)
}) }
像其他练习一样我们创建了一个辅助函数,但不同的是我们传入了一个 Shape 类型。如果没有定义 Shape 类型编译会报错。
 怎样定义 Shape 类型呢?我们用一个 Go 语言的接口定义来声明 Shape 类型:

type Shape interface {
Area() float64
}
这样我们就像创建 RectangleCircle 一样创建了一个新类型,不过这次是 interface 而不是 struct。
 
稍等,什么?
这种定义 interface 的方式与大部分其他编程语言不同。通常接口定义需要这样的代码 My type Foo implements interface Bar
但是在我们的例子里,
  •  
    Rectangle 有一个返回值类型为 float64 的方法 Area,所以它满足接口 Shape
  •  
    Circle 有一个返回值类型为 float64 的方法 Area,所以它满足接口 Shape
 
  • string 没有这种方法,所以它不满足这个接口
 
在 Go 语言中 interface resolution 是隐式的。如果传入的类型匹配接口需要的,则编译正确。
 

解耦

请注意我们的辅助函数是怎样实现不需要关心参数是矩形,圆形还是三角形的。通过声明一个接口,辅助函数能从具体类型解耦而只关心方法本身需要做的工作。
 这种方法使用接口来声明我们仅仅需要的。这种方法在软件设计中非常重要,我们以后在后续部分中还是涉及到更多细节。

进一步重构

现在我们对结构体有一定的理解了,我们可以引入「表格驱动测试」。
表格驱动测试在我们要创建一系列相同测试方式的测试用例时很有用。

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)
}
} }
这里唯一的新语法是创建了一个匿名的结构体。我们用含有两个域 shape 和 want 的 []struct 声明了一个结构体切片。
然后我们用测试用例去填充这个数组了。
我们可以像遍历任何其他切片一样来遍历这个数组,进而用这个结构体的域来做我们的测试。
你会看到开发人员能方便的引入一个新的几何形状,只需实现 Area 方法并把新的类型加到测试用例中。
另外发现 Area 方法有错误,我们可以在修复这个错误之前非常容易的添加新的测试用例。
列表驱动测试可以成为你工具箱中的得力武器。但是确保你在测试中真的需要使用它。
如果你要测试一个接口的不同实现,或者传入函数的数据有很多不同的测试需求,这个武器将非常给力。
 让我们通过再添加一个三角形并测试它来演示所有这些技术。
 

先写测试函数

为我们的新类型添加测试用例非常容易,只需添加 "{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
}
最后代码编译通过。运行后得到如下错误:
 shapes_test.go:31: got 0.00 want 36.00
编写正确的代码让测试函数通过

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},
这些数字代表什么并不一目了然,我们应该让我们的测试函数更容易理解。
 到目前为止我们仅仅学到一种创建结构体 MyStruct{val1, val2} 的方法,但是我们可以选择命名这些域。就像如下代码所示:

    {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
我们知道它仅仅和三角形有关,但是如果在一个包含二十个测试用例的系统里出现类似的错误呢?开发人员如何知道是哪个测试用例失败了呢?这对于开发人员来说不是一个好的体验,他们需要手工检查所有的测试用例以定位到哪个用例失败了。
我们可以改进我们的错误输出为 "%#v got %.2f want %.2f. %#v",这样会打印结构体中域的值。这样开发人员能一眼看出被测试的属性。
关于列表驱动测试的最后一点提示是使用 t.Run。
 在每个用例中使用 t.Run,测试用例的错误输出中会包含用例的名字:

-------- 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 这样的静态语言中,能定义自己的类型是开发易维护,低耦合,好测试的软件的基础。
接口是把负责从系统的其他部分隐藏起来的伟大工具。在我们的测试中,辅助函数的代码不需要知道具体的几何形状,只需要知道获取它的面积即可。
当你以后更熟悉 Go 后你会发现接口和标准库的真正威力。你会看到标准库中的随处可见的接口。通过在你自己的类型中实现这些接口你能很快的重用大量的伟大功能。
 
 

Go语言:利用 TDD 驱动开发测试 学习结构体、方法和接口的更多相关文章

  1. C语言开发函数库时利用不透明指针对外隐藏结构体细节

    1 模块化设计要求库接口隐藏实现细节 作为一个函数库来说,尽力降低和其调用方的耦合.是最主要的设计标准. C语言,作为经典"程序=数据结构+算法"的践行者,在实现函数库的时候,必定 ...

  2. C语言中两个相同类型的结构体变量之间是可以相互直接赋值的

    C语言中,在相同类型的变量间赋值时是直接内存复制的,即将他们的内存进行复制,而两个同类型的结构体变量属于同一种变量,所以赋值时是按照他们的内存分布来直接拷贝的.所以,在C语言中两个相同类型的结构体变量 ...

  3. Windows内核驱动开发入门学习资料

    声明:本文所描述的所有资料和源码均搜集自互联网,版权归原始作者所有,所以在引用资料时我尽量注明原始作者和出处:本文所搜集资料也仅供同学们学习之用,由于用作其他用途引起的责任纠纷,本人不负任何责任.(本 ...

  4. ios开发中的C语言学习—— 结构体简介

    在开发过程中,经常会需要处理一组不同类型的数据,比如学生的个人信息,由姓名.年龄.性别.身高等组成,因为这些数据是由不同数据类型组成的,因此不能用数组表示,对于不同数据类型的一组数据,可以采用结构体来 ...

  5. go语言学习-结构体

    结构体 go语言中的结构体,是一种复合类型,有一组属性构成,这些属性被称为字段.结构体也是值类型,可以使用new来创建. 定义: type name struct { field1 type1 fie ...

  6. c语言学生信息管理系统-学习结构体

    #include<stdio.h> #include<stdlib.h> //结构体可以存放的学生信息最大个数,不可变变量 ; //学生信息结构体数组,最多可以存放100个学生 ...

  7. 29个android开发常用的类、方法及接口

    在安卓开发中,我们常常都需要借助各种各样的方法.类和接口来实现相关功能.提升开发效率,但对于初学者而言,什么时候该用什么类.方法和接口呢?下面小编整理了29个,日常开发中比较常用的类.方法.接口及其应 ...

  8. Go语言 - 结构体 | 方法

    自定义类型和类型别名 自定义类型 在Go语言中有一些基本的数据类型,如string.整型.浮点型.布尔等数据类型, Go语言中可以使用type关键字来定义自定义类型. 自定义类型是定义了一个全新的类型 ...

  9. 全国计算机等级考试二级教程-C语言程序设计_第14章_结构体、共用体和用户定义类型

    函数的返回值是结构体类型 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> struct ...

  10. 网络驱动移植之net_device结构体及其相关的操作函数

    内核源码:Linux-2.6.38.8.tar.bz2 在Linux系统中,网络设备都被抽象为struct net_device结构体.它是网络设备硬件与上层协议之间联系的接口,了解它对编写网络驱动程 ...

随机推荐

  1. 记一次前端ajax禁止使用异步async的操作

    环境: 前端layui  jquery 情况: 页面在iframe里面, 然后点击按钮,弹出输入框.点击确认,弹出框发送内容到后台, 传送数据到后台后,然后根据返回一个map给前端.前端解析数据,返回 ...

  2. python爬虫实战——自动下载百度图片(文末附源码)

    用Python制作一个下载图片神器 前言 这个想法是怎么来的? 很简单,就是不想一张一张的下载图片,嫌太慢. 在很久很久以前,我比较喜欢收集各种动漫的壁纸,作为一个漫迷,自然是能收集多少就收集多少.小 ...

  3. sdio/mmc/sd笔记

    [SDIO] SD card 初始化及常用命令解析 https://blog.csdn.net/u010443710/article/details/107014873 cmd0命令,是单向命令,ho ...

  4. vue中关于get传参数为数组的解决方法

    按理来说,get请求方式是没有数组的,get请求方式带参数都是字符串,需要和后端协商是用某个标识符分割开,例如"|"   ",". 当然如果需要数组的话,也能解 ...

  5. 关闭Google自动更新

    一.禁用任务计划 二.禁用更新服务 三.重命名更新程序 首先找到谷歌浏览器的安装位置

  6. DEM高程数据下载资源

    最近发现了几个比较好的DEM高程数据免费下载资源,遂总结一下. clouldRF(https://cloudrf.com/terrain%20data)官方网站有说明其支持的地形数据来源,主要包括如下 ...

  7. 简单了解promise

    promise是什么: JavaScript中存在很多异步操作, Promise将异步操作队列化,按照期望的顺序执行,返回 符合预期的结果.可以通过链式调用多个 Promise达到我们的目的. Pro ...

  8. Sql Server新建一个只读权限的用户

    1,新建只能访问某一个表的只读用户. --添加只允许访问指定表的用户: exec sp_addlogin '用户名','密码','默认数据库名' --添加到数据库 exec sp_grantdbacc ...

  9. Web _Servlet(url-pattern)的配置与优先级

    url-pattern的配置方式有三种: 1.完全路径匹配:以  '/'  开始 例: /ServletDemo1  , /aaa/ServletDemo2 , /aa/bb/ServletDemo3 ...

  10. FCC 高级算法题 库存更新

    Inventory Update 依照一个存着新进货物的二维数组,更新存着现有库存(在 arr1 中)的二维数组. 如果货物已存在则更新数量 . 如果没有对应货物则把其加入到数组中,更新最新的数量. ...