3.04 路由树-静态匹配测试用例

上节课已经跑通了第1个测试用例,这里再重申一次TDD的步骤:

  1. 定义API

  2. 定义测试

  3. 添加测试用例

  4. 实现,并且确保实现能够通过测试用例

  5. 重复3-4直到考虑了所有的场景

  6. 重复步骤1-5

接下来要做的就是不断添加测试用例.

PART1. 添加测试用例

1.1 添加根节点的测试用例

为什么要单独给根节点添加测试用例?

注意我们的AddRoute()方法中有这样一段代码:

// step1. 找到路由树
root, ok := r.trees[method]
// 如果没有找到路由树,则创建一棵路由树
if !ok {
	root = &node{
		path: "/",
	}
	r.trees[method] = root
}

可以看到逻辑上对根节点是有特殊处理的.

  • 初态工程结构如下:

1.1.1 为根节点添加测试用例

router_test.go:

此处改动了2处:

  • 添加了1个测试用例

  • wantRouter中为根节点添加了HandleFunc

其实稍微想一下就知道,我们的测试用例现在肯定跑不起来,因为我们在AddRoute()方法中把path的前导/给直接删掉了.导致按/切割path的结果是一个[]string{""}.HandleFunc加的位置不对.

1.1.2 为根节点添加特殊处理的代码

修这个bug的思路很简单:如果path是/,直接添加HandleFunc并返回即可

注意:这里也是通过IDE的Debug功能调试的

Tips:

  • 注意Debug时控制台的Resume Program按钮是指执行下一次函数的意思.这个在调试时很有用

  • Debug时可以点变量值左侧的调用栈看每个函数里当时的情况

router.go:

这样就可以通过测试了.

1.2 添加前导/user节点的测试用例

router_test.go:

此处改动了2处:

  • 添加了1个测试用例

  • wantRouter中为user节点添加了HandleFunc

这次是可以成功通过测试的.

1.3 添加/order/detail节点的测试用例

该用例是为了测试当路由树中间有不存在的节点(即order节点)时,AddRoute()方法是否符合预期.

router_test.go:

此处改动了2处:

  • 添加了1个测试用例

  • wantRouter中添加了order节点,并为该节点设置了子节点detail,且为detail子节点添加了HandleFunc

这次是可以成功通过测试的.

1.4 为其他HTTP动词添加测试用例

此处为POST方法的/order/create路由添加测试用例.

router_test.go:

此处改动了3处:

  • 添加了1个测试用例

  • wantRouter中添加了POST方法的路由树

  • wantRouter中添加了order节点,并为该节点添加了子节点create,且为create子节点添加了HandleFunc

这次是可以成功通过测试的.

1.5 再次为POST方法添加测试用例

这个测试用例是为了测试POST方法中路由仅有1段的case.

添加/login路由的测试用例:

router_test.go:

此处改动了2处:

  • 添加了1个测试用例

  • wantRouter中的POST路由树上添加了login节点,且为该节点添加了HandleFunc

这次是可以成功通过测试的.

PART2. 全静态路由的非法用例

初态工程结构如下:

2.1 字面量为空字符串的路由

2.1.1 添加对path为空字符串的限制

这里只需要在AddRoute()方法中限制path不能为空字符串即可:

router.go:

2.1.2 添加对应的测试用例

测试用例如下:

router_test.go:

注意这时我们测试的是触发panic的用例,因此要使用assert.Panics()进行断言

注意

  • 这时我们测试的是触发panic的用例,因此要使用assert.Panics()进行断言

  • 文件还是router_test.go,只是我觉得再在TestRouter_AddRoute()函数上加测试用例就太乱了,而且本小节的目标就是对各种非法路由(或者可以说边缘case)做测试,所以我单独又写了一个测试函数.这里为了避免笔记看上去太乱了,所以只记录了新写的这个测试函数.

2.2 不是以/为前导字符的路由

从PART1的测试情况来看,似乎一切良好.但其实我们忽略了一个case:**如果路由不是以/前导的怎么办?**比如注册的路由为login,而不是/login的情况

注意:断点调试时,IDE上点(注意是拿鼠标点的不是拿代码打的)好断点之后,右键可以为断点设置条件,当符合条件时断点才生效.

为断点设置条件

注意:

这里因为我当时切割path时用的strings.TrimLeft(path, "/"),所以没触发这个case.

看起来似乎一切正常,甚至超出预期:我们还支持了以"前导字符不为/"的路由形式.此时再思考一个问题:我们设计的框架究竟应不应该支持这种路由?

我个人认为不应该支持.因为这样使用你的框架去写代码的人,他写出来的代码,可读性反而会变差.举个例子:

  • 如果路由是/login,那么很容易从注册路由时的代码看出其路由的全貌

  • 如果路由是login,那么阅读他的代码的人,也很自然的会去联想:

    • 这个login前边是不是还有什么前缀?

    • 会不会有一些类似namespace的隔离机制将"前导字符为/的路由"与"前导字符不为/的路由"分隔开?

    • "前导字符为/的路由"与"前导字符不为/的路由"有什么区别?

可以想象到,如果我们设计成支持"前导字符不为/的路由",反而做了一件费力不讨好的事情.

2.2.1 对不是以/为前导字符的路由进行限制

这里只需要在AddRoute()方法中限制path必须以/开头即可

router.go:

2.2.2 添加对应的测试用例

router_test.go:

2.3 以/结尾的路由

2.3.1 对以/结尾的路由进行限制

这里只需要在AddRoute()方法中限制path不能以/结尾即可

router.go:

2.3.2 添加对应的测试用例

router_test.go:

2.4 包含连续的/路由

2.4.1 对包含连续的/路由进行限制

这里老师的检测方式很巧妙:直接判断按/分割path后的字符串切片中,是否包含空字符串即可.有空字符串就说明path中包含连续的/.

TODO:这里这一周的课程结束后要去试一下我想象中的先检测后进逻辑的方案

router.go:

2.4.2 添加对应的测试用例

router_test.go:

2.5 路由的重复注册问题

路由重复注册:即对一个已经存在于路由树中的路由进行再次注册.

这里我们的设计是:不允许使用者注册一个已经存在于路由树中的路由

判断的依据也很简单:找到目标节点之后,检测该节点是否存在HandleFunc即可.

2.5.1 根节点的重复注册问题

a. 测试用例

router_test.go:

b. 添加对根节点的路由重复注册限制

router.go:

2.5.2 普通节点的重复注册问题

a. 测试用例

router_test.go:

b. 添加对普通节点的路由重复注册限制

router.go:

PART3. 问题:AddRoute()方法的method参数需要校验吗?

这个其实不需要校验,因为调用AddRoute()方法的代码都是咱自己写的代码.而非框架使用者写的.

为了确保这一点,需要将AddRoute()方法改为addRoute()方法.

router.go:

也将Server接口的注册路由方法改为私有即可:

serverInterface.go:

最后再改所有调用了AddRoute()的地方就可以了(httpServer.gorouter_test.go,此处就不贴代码了,自己command + r一下就行了).

PART4. 问题:addRoute()方法的handleFunc参数需要校验吗?

不需要.因为正常的使用者都不会传个nil进来的.按照我们现在的设计,他传个nil进来,就相当于没有注册这个路由.再者来讲,从使用者的视角上来看,path字段确实容易写错,但是handleFunc基本上正常使用的情况下都不会写错的.因此没必要校验.

Last updated