记一次测试驱动开发练习
乐乎为什么要尝试测试驱动
从毕业工作到现在,自己也是个十年的码农了。十年,可以让新手变成专家。但如果把一年的经验用了十年,新手还是新手。很不幸,我是后者。以前的工作经历都是单打独斗,来活了就是短平快,一把梭。这样会有什么积累呢?
最近进入了新的项目组,团队合作的项目明显多了。自己提交的代码遭到了客观的差评,脸上有点火辣辣的。自己确实和优秀还有很大的进步空间。大概是3月份加班忙的头昏脑胀的时候,一起加班的 leader 给了我一些学习的建议,简单的说书看的太少,思考太少。
在项目的业务代码开发完成后, leader 说让大家写一些单元测试。期间对自己写的单元测试不是很满意,空时找了一本书《有效的单元测试》看了一遍。对于为什么要写单元测试我是很认可的。
- 自己的代码提交改动经常很大,commit 没有层次性
- 项目开发过程中调整代码结构,导致逻辑改东忘西
- 每次改动后都要把工程run起来,关键的 case 手动跑一遍
对于一些写单元测试的困惑
- 面对一个方法,脑子里觉得他太简单了,甚至不需要测试
- 分不清测试的层次,有的单元测试写的特别粗,有的单元测试又过于细节
这本书目前只读了2/3,书中的一些理念基本上已经解决了我的困惑。作者讲到他在一家公司中看到一个同事的编码风格后很受启发:
Sebastian 的编码风格是先写一个会失败(很明显是这样)的测试,再写足够使测试通过的代码,然后再编写另一个失败的测试,他重复这个循环直到完成任务。与 Sebastian 共事,Marcus 意识到自己的编程风格开始转变。他的对象结构不同了,代码看起来稍微不同了,只是因为他从调用者的角度来审视自己代码的设计和行为了。
这本书并不是讲 tdd 的,只是介绍了 tdd 的理念。之前看过一些重构,编码风格的书,虽然产生了一些共鸣,但是很快就忘记了。想来是自己没有通过练习转化成自己内在的东西,纸上得来终觉浅,觉知此事要躬行, 正好工作上接入了一个对接广告系统的活,业务不是很复杂,时间比较宽裕,于是就有了这次练习。希望可以通过练习把它变成自己的习惯。
需求背景
业务上需要对接 RTA 广告投放的功能,广告平台在竞价时会调用广告主的 RTA 服务,请求参数是:本次可参与竞价的账户列表。广告主根据账户规则配置,返回参与竞价的账户,价格,以及个性化的素材。主要解决了广告投放中的人群定向、素材个性化问题(指定哪些用户参与竞价,以及展示的广告内容是什么)。
本次的账户规则主要有:
- 用户活跃等级:
- 根据用户最近未活跃的天数判断账户是否参与竞价,以及出价
- 账户类型:
- 根据账户类型判断账户是否参与竞价,以及出价
- 品类账户,如果用户的偏好分类与账户品类匹配,返回出价
- dpa账户,如果用户有可用的 sku_list,参与竞价,返回 sku_list
- 安装app规则:
- 根据规则判断用户是否安装/卸载了某个app,满足条件后出价
- 支付能力:
- 根据规则判断用户的支付能力等级,满足条件后出价
拆分子问题
根据需求,下面拆分一下需要解决的子问题。每个账户需要一个规则配置?规则之间的关联关系是什么?用户数据从哪里来?
- 账户规则配置
- 账户的出价行为
- 一个账户下可以配置不同种类的出价规则 逻辑关系 and 命中的多个类型规则,出价相乘,skulist 叠加.
- 同类型规则下面可以配置多个规则 ,逻辑关系 or 命中多个规则出价取最大,skulist 叠加。
- 数据访问
- 用户付费等级
- 用户 App 安装列表
- 用户偏好:偏好品类,偏好 sku
- 用户最近活跃时间
本次只把账户的出价行为部分,进行举例
开发过程
明确了需要解决的子问题后,就可以以尝试进行开发了。按照测试驱动的原则,我需要构建一个单元测试,面临的第一个问题是:写单元测试的时候可能没有任何的结构体,方法。这个时候我需要先准备一些程序骨架,然后编写单元测试,最后编写代码使测试通过。定义程序骨架的时候,我倾向于坚持只定义必要的结构体、函数、接口。而不写任何多余的代码。
准备程序骨架
定义账户出价规则的配置类
AccountRuleConf
,包含了各种类型的规则。定义
bider
接口,表示出价行为。每种具体的规则需要实现bider
接口。定义
RTA
方法 业务方法的主入口,描述了业务的主要行为:广告平台询问需要竞价的账户,返回那些账户竞价,用什么素材竞价。定义
getAccountRuleGroup
方法,来对每个账户配置的规则进行分类,同种类型规则的关系是 or ,不同类型的规则关系是 and。相关代码如下:账户配置类
// 账户出价配置 type AccountRuleConf struct { // 用户活跃类型规则 []*ActiveLevelRule ActiveRules // 广告账户类型规则 *AccountTypeRule AccountRule // 设备安装 app 类型规则 []*AppRule AppInstallRules // 用户支付等级类型规则 []*PayLevelRule PayLevelRules } // 用户活跃等级规则 type ActiveLevelRule struct { } // 账户类型规则 type AccountTypeRule struct { } // app 安装规则 type AppRule struct { } // 支付等级规则 type PayLevelRule struct { }
service 层骨架代码
// 出价接口返回数据 type BidData struct { bool // 是否出价 IsBid float64 // 出价比例 Ratio []int // 参与竞价的素材 SKUList } // 用户数据 type User struct { } // 出价接口判断规则是否需要出价 type bider interface { (ud User) (bidData BidData, err error) Bid} // 业务处理入口函数 func RTA(mid int, accounts []int) (res map[int]BidData, err error) { return } // 返回每个账号的规则组 func getAccountRuleGroup(accountID int) [][]bider { return nil }
第一个单元测试
有了基本的程序骨架,就可以构建测试用例,我选择了从 RTA
方法作为入口写第一个单元测试。首先需要明确的测试的目的,关注的是账户规则的出价行为,而出价行为包括了多规则组,规则组下多规则的情况。这时遇到了一些问题: 测试 RTA
方法的单元测是否要构建一个具体的账户出价规则比如[用户活跃规则A出价&&用户活跃规则B不出价]?这显然是不需要的。如何 mock 单个规则的出价返回值 ? 由于每个具体的规则 xxRule
都会实现 bider
接口,这样就可以不关心具体的 xxRule
的实现细节,通过构建一个 mockRule
来进行出价行为的 mock ,通过打桩操作 mock getAccountRuleGroup 方法返回一个 [][]{mockBidRule{}}
,即可表示多个规则组的不同规则的出价行为。
- 目的:测试 RTA 方法账户出价返回值
- 输入:单个账号,单个规则组,单规则不出价
- 输出:账户不出价
- 代码:
// 构建用于 mock 单规则出价行为
type mockBidRule struct {
ret BidData}
// 实现 bider 接口
func (m mockBidRule) Bid(u User) (BidData, error) {
return m.ret, nil
}
func TestRTAWithOneNoBidRule(t *testing.T) {
// 单规则,不出价
:= BidData{IsBid: false}
inputRuleBid
// mock 某个账号的出价规则
:= mock.Patch(getAccountRuleGroup, func(id int) (rule [][]bider) {
m1 // 通过 mockBidRule 模拟了某个规则的出价行为
= [][]bider{
rule []bider{mockBidRule{ret: inputRuleBid}},
}
return
})
defer m1.Unpatch()
:= 1
mid := 11
accountID , _ := RTA(mid, []int{accountID})
res// 该账号不出价
.False(t, res[accountID].IsBid)
assertreturn
}
单元测试写好了以后,可以 go test
看一下单元测试的结果,由于 go 语言默认的问题,这个单元测试并没有报错。原因是 RTA
返回的 res
是一个空 map
, res[accountID]
得到的是一个空的结构体,空结构体的 bool
类型的属性默认值是 false
所以 assert.False((t, res[accountID].IsBid)
的断言不会报错。这里是我第一步的失误之处,还是要先写一个明显失败的单元测试,然后填充代码使得单元测试通过,这样能够验证程序确实有在执行工作。
由于第一个单元测试就是通过的,继续填充代码的操作也是无效的。所以再来写一个真第一个单元测试
[真] 第一个单元测试
- 目的:测试 RTA 方法账户出价返回值
- 输入:单规则组、单规则出价、规则返回 sku
- 输出:账号出价,返回 sku
- 代码:
func TestRTAWithOneBidRule(t *testing.T) {
// 单规则、出价、返回 sku
:= BidData{IsBid: true, Ratio: 1, SKUList: []int{1}}
inputRuleBid // 账号出价、返回 sku
:= BidData{IsBid: true, Ratio: 1, SKUList: []int{1}}
assertAccountBid
// mock 某个账号的出价规则
:= mock.Patch(getAccountRuleGroup, func(id int) (rule [][]bider) {
m1 // 通过 mockBidRule 模拟了某个规则的出价行为
= [][]bider{
rule []bider{mockBidRule{ret: inputRuleBid}},
}
return
})
defer m1.Unpatch()
:= 1
mid := 11
accountID , _ := RTA(mid, []int{accountID})
res.Equal(t, assertAccountBid, res[accountID])
assertreturn
}
这次 go test
后,提示测试用例失败了,可以进行代码填充了。
通过第一个单元测试
func RTA(mid int, accounts []int) (res map[int]BidData, err error) {
= map[int]BidData{}
res // 单元测试中没有依赖 user 的数据,可以先给个空
:= User{}
u for _, account := range accounts {
// 获取某个账号的规则
:= getAccountRuleGroup(account)
group for _, g := range group {
for _, r := range g {
, _ := r.Bid(u)
ruleBidif ruleBid.IsBid {
[account] = ruleBid
resreturn
}
}
}
}
return
}
这里为了示例代码简单,我省略了一些 go 语言中的 if err != nil
的处理。开发过程中,只填充了部分使单元测试通过的代码。这段代码中有明显的逻辑问题,比如单个规则组中多规则、以及多个规则组不同出价情况的处理。但本次填充的代码足够使单元测试通过了,那就可以进行提交了,不写过多代码的原因,可能是因为过多的代码需要考虑代码结构设计的问题,而过早的考虑这些问题同样会有过度设计,或者错误设计的问题,只有在需要设计结构的时候进行合适的设计。
现在回顾下之前的单元测试,就能看到 bider
接口抽象的作用,以及 mock getAccountRuleGroup
方法模拟不同规则的出价行为的作用了。
第二个单元测试
- 目的:测试 RTA 方法账户出价返回
- 输入:单规则组,多个出价规则出价,返回 sku
- 输出:账号出价,出价比例取命中规则的最大值,sku 合并
- 代码:
func TestRTAWithMultiBidRules(t *testing.T) {
// 单规则组,多个出价规则
:= []BidData{
inputRuleBids {IsBid: true, Ratio: 1, SKUList: []int{1}},
{IsBid: true, Ratio: 2, SKUList: []int{2}},
}
:= BidData{
assertAccountBid : true,
IsBid// 出价取最大
: 2,
Ratio// sku 合并
: []int{1, 2},
SKUList}
// mock 某个账号的出价规则
:= mock.Patch(getAccountRuleGroup, func(id int) (rule [][]bider) {
m1 := []bider{}
g1 for _, bid := range inputRuleBids {
= append(g1, mockBidRule{ret: bid})
g1 }
// 通过 mockBidRule 模拟了某个规则的出价行为
= [][]bider{g1}
rule return
})
defer m1.Unpatch()
:= 1
mid := 11
accountID , _ := RTA(mid, []int{accountID})
res.Equal(t, assertAccountBid, res[accountID])
assertreturn
}
通过第二个单元测试
在第二次填充代码时,需要实现规则组内多个出价规则合并出价结果的逻辑,代码如下:
// 业务处理入口函数
func RTA(mid int, accounts []int) (res map[int]BidData, err error) {
= map[int]BidData{}
res // 单元测试中没有依赖 user 的数据,可以先给个空
:= User{}
u for _, account := range accounts {
// 获取某个账号的规则
:= getAccountRuleGroup(account)
group for _, g := range group {
:= BidData{}
groupBid for _, r := range g {
, _ := r.Bid(u)
ruleBid// 规则组内多个规则出价,出价结果合并
if ruleBid.IsBid {
.IsBid = true
groupBid.Ratio = math.Max(groupBid.Ratio, ruleBid.Ratio)
groupBid.SKUList = append(groupBid.SKUList, ruleBid.SKUList...)
groupBid}
}
if groupBid.IsBid {
[account] = groupBid
res}
}
}
return
}
第三个单元测试
- 目的:测试 RTA 方法账户出价返回
- 输入:多个规则组出价,返回 sku
- 输出: 账号出价,出价比例取多组返回出价比例相乘,sku 合并
- 代码:
func TestRTAWithMultiBidGroups(t *testing.T) {
// 单规则组,多个出价规则
:= BidData{IsBid: true, Ratio: 3, SKUList: []int{1}}
inputGroupABid := BidData{IsBid: true, Ratio: 2, SKUList: []int{2}}
inputGroupBBid
:= BidData{
assertAccountBid : true,
IsBid// 出价相乘
: 6,
Ratio// sku 合并
: []int{1, 2},
SKUList}
// mock 某个账号的出价规则
:= mock.Patch(getAccountRuleGroup, func(id int) (rule [][]bider) {
m1 := []BidData{inputGroupABid, inputGroupBBid}
groupBids
// 通过 mockBidRule 模拟了某个规则的出价行为
for _, groupBid := range groupBids {
= append(rule, []bider{mockBidRule{ret: groupBid}})
rule }
return
})
defer m1.Unpatch()
:= 1
mid := 11
accountID , _ := RTA(mid, []int{accountID})
res.Equal(t, assertAccountBid, res[accountID])
assertreturn
}
通过第三个单元测试
本次填充代码,需要关注的逻辑是规则组间多个规则组出价,合并出价结果的逻辑
// 业务处理入口函数
func RTA(mid int, accounts []int) (res map[int]BidData, err error) {
= map[int]BidData{}
res // 单元测试中没有依赖 user 的数据,可以先给个空
:= User{}
u for _, account := range accounts {
// 获取某个账号的规则
:= getAccountRuleGroup(account)
group := BidData{}
accountBid for _, g := range group {
:= BidData{}
groupBid for _, r := range g {
, _ := r.Bid(u)
ruleBidif ruleBid.IsBid {
.IsBid = true
groupBid.Ratio = math.Max(groupBid.Ratio, ruleBid.Ratio)
groupBid.SKUList = append(groupBid.SKUList, ruleBid.SKUList...)
groupBid}
}
if groupBid.IsBid {
.IsBid = true
accountBid// 出价系数相乘
.Ratio = math.Max(1, accountBid.Ratio) * math.Max(1, groupBid.Ratio)
accountBid.SKUList = append(accountBid.SKUList, groupBid.SKUList...)
accountBid}
}
if accountBid.IsBid {
[account] = accountBid
res}
}
return
}
到这里 RTA 方法已经有了基本的血肉,其实还有很多 case 没有列举出来,比如规则组之间有一个不出价的规则组,整个账号不出价、以及后续的每种具体规则的实现,这些具体的过程不再赘述,至此我对测试驱动开发的模式已经开始有了一些感觉。他能帮助我们在开发过程中聚焦,我的开发过程始终在聚焦在测试用例描述的业务事件下,编写的代码是站在调用者的角度上思考,每次提交记录也变得有条理,他们都遵循着,构建测试、通过测试的模式。
重构单元测试
目前为止,可以发现对于 RTA 方法的单元测试,不同的 case 散落在各种 TestXXX
方法中,这也是 review 过程中同事帮我提出的建议,单元测试应该更专注事件是什么,行为是什么,期望是什么 而对于数据的准备、校验逻辑要尽量统一。下面开始重构。
首先定义一个 tc struct
来描述测试用例,mock 每个规则组中,不同规则的返回值是什么, 期望的账户出价是什么。对于这个测试用例,他只描述了业务行为。这就是一个非常明确的测试用例。
这里我使用了 groupARuleBids/groupBRuleBids
表示两个规则组中每个规则不同的出价行为。不用 [][]BidData
表示的原因是希望单元测试更简单,如果测试单个规则组的 case ,另一个 groupXRuleBids
为空即可。 assertAccountBid
表示期望账户的出价。单元测试所专注的行为、事件全部由 tc
来描述,即不同规则组中规则的出价行为,对应账号的出价行为
第二段代码,就是在组织具体的测试用例。这里我发了一个最终版。在这里需要关注的是,尽量避免无关数据的组织,尽量精简这部分的代码。降低阅读代码的成本。这也是我在本次实践中的体会。如果组织测试用例的部分,无关代码太多,构建过程太繁琐。那么可能对于 tc
的抽象还不到位, 还需要继续重构。
第三段代码就是运行构建好的测试用例,根据 tc
的数据构建 mock 需要的参数,执行目标方法,校验期望返回。
func TestRTA(t *testing.T) {
// ***第一段代码
// 测试不同规则组内的规则出价行为所影响的账号出价行为
type tc struct {
// 规则组 A 中的规则出价结果
[]BidData
groupARuleBids // 规则组 B 中的规则出价结果
[]BidData
groupBRuleBids
assertAccountBid BidData// 单元测试报错时,提示的消息
string
msg }
// ***第二段代码
:= []tc{}
cases {
// 单规则组内有多个规则,只有一个规则返回了出价
:= []BidData{
rg1 // 一个出价
{IsBid: true, Ratio: 3, SKUList: []int{1}},
// 一个不出价
{IsBid: false},
}
// 账户出价,返回 sku list
:= BidData{IsBid: true, Ratio: 3, SKUList: []int{1}}
bd = append(cases, tc{groupARuleBids: rg1, assertAccountBid: bd, msg: "t1"})
cases }
{
// 单个规则组多个出价规则命中
:= []BidData{
rg1 {IsBid: true, Ratio: 2, SKUList: []int{1}},
{IsBid: true, Ratio: 3, SKUList: []int{2}},
}
// 出价比例取最大
// sku 合并
:= BidData{IsBid: true, Ratio: 3, SKUList: []int{1, 2}}
bd = append(cases, tc{groupARuleBids: rg1, assertAccountBid: bd, msg: "t2"})
cases }
{
// 测试多个规则组,有一组规则没有命中
:= []BidData{{IsBid: true, Ratio: 2}}
rg1 := []BidData{{IsBid: false, Ratio: 2}}
rg2
// 整个账户不出价
:= BidData{IsBid: false}
bd = append(cases, tc{groupARuleBids: rg1, groupBRuleBids: rg2, assertAccountBid: bd, msg: "t3"})
cases }
{
// 测试多个规则组,多组规则命中
:= []BidData{{IsBid: true, Ratio: 2, SKUList: []int{1}}}
rg1 := []BidData{{IsBid: true, Ratio: 3, SKUList: []int{2}}}
rg2
// 出价系数相乘 2*3
// sku list 叠加
:= BidData{IsBid: true, Ratio: 6, SKUList: []int{1, 2}}
bd = append(cases, tc{groupARuleBids: rg1, groupBRuleBids: rg2, assertAccountBid: bd, msg: "t4"})
cases }
// ***第三段代码
for _, c := range cases {
:= mock.Patch(getAccountRuleGroup, func(id int) (rule [][]bider) {
m1 := [][]BidData{c.groupARuleBids, c.groupBRuleBids}
groupBids
// 通过 mockBidRule 模拟了多个规则组下不同规则的出价行为
for _, group := range groupBids {
:= []bider{}
ruleBids for _, ruleBid := range group {
= append(ruleBids, mockBidRule{ret: ruleBid})
ruleBids }
}
return
})
:= 1
mid := 11
accountID , _ := RTA(mid, []int{accountID})
res
.Unpatch()
m1.Equal(t, c.assertAccountBid, res[accountID], c.msg)
assert}
}
经过重构后,我对单元测试的理解已经有了一些感觉,也非常明确了单元测试所需要聚焦事件而非具体的代码的含义,让我之前对单元测试覆盖的模糊之处又清晰了一些,这里为什么要一直强调测试用例要聚焦业务事件的原因是,单元测试不是把代码通过单元测试钉死在木板上,关注函数的每个分支,每个子函数。这会让单元测试后期的维护产生巨大的工作量,也许只是重构改动了一下函数结构。就会引起单元测试的报错。下面就来看一下,测试驱动对重构的友好体现。
重构 RTA 方法
回顾下之前已经实现的 RTA 方法,可以一眼看到多个 for
循环的嵌套,过多的层次嵌套会导致不在一个抽象层次上的代码混合在了一起,阅读代码的人要在多个层次间切换。也很难得清楚程序的控制流是什么。要一行行的去读,下面就来重构下 RTA 方法吧。
RTA
方法是返回所有出价的账号,那么让这个方法的抽象维度统一在账号出价层,不关心具体的规则组、规则。- 提取
accountBid
方法,这个方法的抽象唯独是某个账号的具体出价规则,该方法在主要体现了一个账号的出价行为由多个规则组/规则组下的多个规则决定的。而具体的组内合并/组间合并出价结果,交给mergeRuleBids/mergeGroupBids
// 业务处理入口函数
func RTA2(mid int, accounts []int) (res map[int]BidData, err error) {
= map[int]BidData{}
res
// 单元测试中没有依赖 user 的数据,可以先给个空
:= User{}
u for _, account := range accounts {
// 获取某个账号的规则
:= getAccountRuleGroup(account)
accountRule := accountBid(accountRule, u)
bid if bid.IsBid {
[account] = bid
res}
}
return
}
func accountBid(rules [][]bider, u User) (res BidData) {
// 收集多个规则组的出价
:= []BidData{}
groupBids for _, g := range rules {
// 收集规则组下多个规则的出价
:= []BidData{}
ruleBids for _, r := range g {
, _ := r.Bid(u)
ruleBid= append(ruleBids, ruleBid)
ruleBids }
// 合并多个规则的出价
:= mergeRulesBids(ruleBids)
groupBid = append(groupBids, groupBid)
groupBids }
// 合并多个规则组的出价
= mergeGroupBids(groupBids)
res return
}
func mergeRulesBids(bids []BidData) (res BidData) {
return
}
func mergeGroupBids(bids []BidData) (res BidData) {
return
}
再来回顾下,为什么测试驱动对重构是友好的,因为在一开始编写单元测试的阶段,我们所聚焦的就不是代码本身的实现,而是业务逻辑,业务行为。如果先开发好 RTA 方法以后再去编写单元测试测,那么以我目前的理解,可能会写出以下聚焦代码本身,而非业务逻辑的单元测试以目前最终的 RTA2 方法为例,
- 测试
RTA
方法 ,mockaccountBid
的返回值,校验RTA
方法的返回值。 - 测试
accountBid
方法,mockmergeRulesBids/mergeGroupBids
方法的返回值,校验accountBid
方法的发挥值 - 测试
mergeRulesBids
,mergeGroupBids
…
简单的说就是用一种机器生成单元测试的方式,为每一个函数生成单元测试 ,每个单元测试只关注本函数的流程控制,通过 mock 的手段 mock 依赖函数的返回值,判断测试函数的期望返回值。 还记得当时看到自己写的单元测试时,还经常困惑于这个方法就一个简单的流程控制还需要写单元测试吗?现在想来有一种把代码钉在案板上的感觉。
当然一些经验非常丰富的高手可能即使后写单元测试,也能写的非常聚焦业务逻辑,非常的有感觉。但这对目前的我来说是困难的。这也许是思维角度的问题,写合适的单元测试是一个好问题。不能为了测试的覆盖率而写单元测试,因为单元测试的代码也是需要维护的。测试驱动从一开始帮你转换了角度,在写单元测试的时候就关注业务逻辑,而非具体的代码实现,代码结构。
关于重构
还想在聊一些关于重构的体会,之前我写代码的时候,经常是面对一个需求,先大概想好了要怎么实现,在实现的过程中发现设计的不合理,可能会直接推倒重来,大改之前已经实现好的部分逻辑。这种破坏性的修改随着需求的规模大小,可能要往返数次。经常会引起一些问题:
- 遗留了一些不再引用的变量
- 改动时因为手误,导致的逻辑缺失、逻辑错误、变量使用错误
这些看似小的问题,往往都在最后的集成测试中发现,甚至带到了线上。虽然我对重构的学习还不够,但已经渐渐明白了重构的一些感觉。
- 好的代码结构不是一开始设计出来的,而是一点点重构来的
- 单元测试也需要重构
- 重构是需要有测试用例保障的基础下重构的,而我之前的那种粗糙的输出模式只是在重写代码
测试驱动在一开始就控制了我的开发节奏,每次行动之前先明确单元测试,每次行动只写一部分可以通过测试的代码。在这个项目初版产生了 100 多个提交,在实践过程中,也有很多次对设计,结构的改动。我发现我的代码结构,甚至抽象结构设计的不合理,不符合需求的时候,在我需要改动他之前。会有单元测试来保证,即使我要做的改动是破坏性的,我也会先按照新的结构来重新改好单元测试,在提交代码。因为这种模式,主动的暴露了很多开发过程中的马虎之处。
另外很重要的一点是,在测试驱动的过程中会让我站在函数调用者的角度,而不是开发者的角度来思考实现和设计
总结
最后来总结下本次实践中,测试驱动所解决自己遇到的问题。
这次练习只是初步的体会下测试驱动带来的好处,后面会多看一些书,总结下比较好的方法论, 应用在开发过程中。