3.04 路由树-静态匹配测试用例
上节课已经跑通了第1个测试用例,这里再重申一次TDD的步骤:
定义API
定义测试
添加测试用例
实现,并且确保实现能够通过测试用例
重复3-4直到考虑了所有的场景
重复步骤1-5
接下来要做的就是不断添加测试用例.
PART1. 添加测试用例
1.1 添加根节点的测试用例
为什么要单独给根节点添加测试用例?
注意我们的AddRoute()
方法中有这样一段代码:
// step1. 找到路由树
root, ok := r.trees[method]
// 如果没有找到路由树,则创建一棵路由树
if !ok {
root = &node{
path: "/",
}
r.trees[method] = root
}
可以看到逻辑上对根节点是有特殊处理的.
初态工程结构如下:
(base) yanglei@yuanhong 08-staticMatchingTestCase % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go
0 directories, 8 files
1.1.1 为根节点添加测试用例
router_test.go
:
package staticMatchingTestCase
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
method string
path string
}
// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/",
},
}
r := newRouter()
mockHandleFunc := func(ctx Context) {}
for _, testRoute := range testRoutes {
r.AddRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树 断言二者是否相等
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
// /user和/ 都是没有handleFunc的
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
},
}
// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
// assert.Equal(t, wantRouter, r)
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
// 如果目标路由森林为nil 则不相等
if y == nil {
return fmt.Sprintf("目标路由森林为nil"), false
}
// 如果两个路由森林中的路由树数量不同 则不相等
rTreesNum := len(r.trees)
yTreesNum := len(y.trees)
if rTreesNum != yTreesNum {
return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
}
for method, tree := range r.trees {
dstTree, ok := y.trees[method]
// 如果目标router中没有对应HTTP方法的路由树 则不相等
if !ok {
return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
}
// 比对两棵路由树的结构是否相等
msg, equal := tree.equal(dstTree)
if !equal {
return method + "-" + msg, false
}
}
return "", true
}
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
// 如果目标节点为nil 则不相等
if y == nil {
return fmt.Sprintf("目标节点为nil"), false
}
// 如果两个节点的path不相等 则不相等
if n.path != y.path {
return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
}
// 若两个节点的子节点数量不相等 则不相等
nChildrenNum := len(n.children)
yChildrenNum := len(y.children)
if nChildrenNum != yChildrenNum {
return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
}
// 若两个节点的handleFunc类型不同 则不相等
nHandler := reflect.ValueOf(n.HandleFunc)
yHandler := reflect.ValueOf(y.HandleFunc)
if nHandler != yHandler {
return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
}
// 比对两个节点的子节点映射是否相等
for path, child := range n.children {
dstChild, ok := y.children[path]
// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
if !ok {
return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
}
// 比对两个子节点是否相等
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
此处改动了2处:
添加了1个测试用例
wantRouter
中为根节点添加了HandleFunc
其实稍微想一下就知道,我们的测试用例现在肯定跑不起来,因为我们在AddRoute()
方法中把path
的前导/
给直接删掉了.导致按/
切割path
的结果是一个[]string{""}
.HandleFunc加的位置不对.
1.1.2 为根节点添加特殊处理的代码
修这个bug的思路很简单:如果path是/
,直接添加HandleFunc并返回即可
注意:这里也是通过IDE的Debug功能调试的
Tips:
注意Debug时控制台的
Resume Program
按钮是指执行下一次函数的意思.这个在调试时很有用Debug时可以点变量值左侧的调用栈看每个函数里当时的情况
router.go
:
package staticMatchingTestCase
import "strings"
// router 路由森林 用于支持对路由树的操作
type router struct {
// trees 路由森林 按HTTP动词组织路由树
// 该map中 key为HTTP动词 value为路由树的根节点
// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
trees map[string]*node
}
// newRouter 创建路由森林
func newRouter() *router {
return &router{
trees: map[string]*node{},
}
}
// AddRoute 注册路由到路由森林中的路由树上
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
// step1. 找到路由树
root, ok := r.trees[method]
// 如果没有找到路由树,则创建一棵路由树
if !ok {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step2. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
if path == "/" {
root.HandleFunc = handleFunc
return
}
// step3. 切割path
// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
// path = path[1:]
path = strings.TrimLeft(path, "/")
segments := strings.Split(path, "/")
// step3. 为路由树添加路由
// Tips: 此处我认为用target指代要添加路由的节点更好理解
target := root
for _, segment := range segments {
// 如果路由树中途有节点没有创建,则创建该节点;
// 如果路由树中途存在子节点,则找到该子节点
child := target.childOrCreate(segment)
// 继续为子节点创建子节点
target = child
}
// 为目标节点设置HandleFunc
target.HandleFunc = handleFunc
}
这样就可以通过测试了.
1.2 添加前导/user
节点的测试用例
/user
节点的测试用例router_test.go
:
package staticMatchingTestCase
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
method string
path string
}
// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
}
r := newRouter()
mockHandleFunc := func(ctx Context) {}
for _, testRoute := range testRoutes {
r.AddRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树 断言二者是否相等
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
// /user和/ 都是没有handleFunc的
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
},
}
// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
// assert.Equal(t, wantRouter, r)
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
// 如果目标路由森林为nil 则不相等
if y == nil {
return fmt.Sprintf("目标路由森林为nil"), false
}
// 如果两个路由森林中的路由树数量不同 则不相等
rTreesNum := len(r.trees)
yTreesNum := len(y.trees)
if rTreesNum != yTreesNum {
return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
}
for method, tree := range r.trees {
dstTree, ok := y.trees[method]
// 如果目标router中没有对应HTTP方法的路由树 则不相等
if !ok {
return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
}
// 比对两棵路由树的结构是否相等
msg, equal := tree.equal(dstTree)
if !equal {
return method + "-" + msg, false
}
}
return "", true
}
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
// 如果目标节点为nil 则不相等
if y == nil {
return fmt.Sprintf("目标节点为nil"), false
}
// 如果两个节点的path不相等 则不相等
if n.path != y.path {
return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
}
// 若两个节点的子节点数量不相等 则不相等
nChildrenNum := len(n.children)
yChildrenNum := len(y.children)
if nChildrenNum != yChildrenNum {
return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
}
// 若两个节点的handleFunc类型不同 则不相等
nHandler := reflect.ValueOf(n.HandleFunc)
yHandler := reflect.ValueOf(y.HandleFunc)
if nHandler != yHandler {
return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
}
// 比对两个节点的子节点映射是否相等
for path, child := range n.children {
dstChild, ok := y.children[path]
// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
if !ok {
return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
}
// 比对两个子节点是否相等
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
此处改动了2处:
添加了1个测试用例
wantRouter
中为user
节点添加了HandleFunc
这次是可以成功通过测试的.
1.3 添加/order/detail
节点的测试用例
/order/detail
节点的测试用例该用例是为了测试当路由树中间有不存在的节点(即order
节点)时,AddRoute()
方法是否符合预期.
router_test.go
:
package staticMatchingTestCase
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
method string
path string
}
// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
}
r := newRouter()
mockHandleFunc := func(ctx Context) {}
for _, testRoute := range testRoutes {
r.AddRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树 断言二者是否相等
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
// /user和/ 都是没有handleFunc的
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
},
}
// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
// assert.Equal(t, wantRouter, r)
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
// 如果目标路由森林为nil 则不相等
if y == nil {
return fmt.Sprintf("目标路由森林为nil"), false
}
// 如果两个路由森林中的路由树数量不同 则不相等
rTreesNum := len(r.trees)
yTreesNum := len(y.trees)
if rTreesNum != yTreesNum {
return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
}
for method, tree := range r.trees {
dstTree, ok := y.trees[method]
// 如果目标router中没有对应HTTP方法的路由树 则不相等
if !ok {
return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
}
// 比对两棵路由树的结构是否相等
msg, equal := tree.equal(dstTree)
if !equal {
return method + "-" + msg, false
}
}
return "", true
}
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
// 如果目标节点为nil 则不相等
if y == nil {
return fmt.Sprintf("目标节点为nil"), false
}
// 如果两个节点的path不相等 则不相等
if n.path != y.path {
return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
}
// 若两个节点的子节点数量不相等 则不相等
nChildrenNum := len(n.children)
yChildrenNum := len(y.children)
if nChildrenNum != yChildrenNum {
return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
}
// 若两个节点的handleFunc类型不同 则不相等
nHandler := reflect.ValueOf(n.HandleFunc)
yHandler := reflect.ValueOf(y.HandleFunc)
if nHandler != yHandler {
return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
}
// 比对两个节点的子节点映射是否相等
for path, child := range n.children {
dstChild, ok := y.children[path]
// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
if !ok {
return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
}
// 比对两个子节点是否相等
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
此处改动了2处:
添加了1个测试用例
wantRouter
中添加了order
节点,并为该节点设置了子节点detail
,且为detail
子节点添加了HandleFunc
这次是可以成功通过测试的.
1.4 为其他HTTP动词添加测试用例
此处为POST方法的/order/create
路由添加测试用例.
router_test.go
:
package staticMatchingTestCase
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
method string
path string
}
// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodPost,
path: "/order/create",
},
}
r := newRouter()
mockHandleFunc := func(ctx Context) {}
for _, testRoute := range testRoutes {
r.AddRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树 断言二者是否相等
wantRouter := &router{
trees: map[string]*node{
// GET方法路由树
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": &node{
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
// /user和/ 都是没有handleFunc的
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
// POST方法路由树
http.MethodPost: &node{
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: map[string]*node{
"create": &node{
path: "create",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: nil,
},
},
}
// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
// assert.Equal(t, wantRouter, r)
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
// 如果目标路由森林为nil 则不相等
if y == nil {
return fmt.Sprintf("目标路由森林为nil"), false
}
// 如果两个路由森林中的路由树数量不同 则不相等
rTreesNum := len(r.trees)
yTreesNum := len(y.trees)
if rTreesNum != yTreesNum {
return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
}
for method, tree := range r.trees {
dstTree, ok := y.trees[method]
// 如果目标router中没有对应HTTP方法的路由树 则不相等
if !ok {
return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
}
// 比对两棵路由树的结构是否相等
msg, equal := tree.equal(dstTree)
if !equal {
return method + "-" + msg, false
}
}
return "", true
}
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
// 如果目标节点为nil 则不相等
if y == nil {
return fmt.Sprintf("目标节点为nil"), false
}
// 如果两个节点的path不相等 则不相等
if n.path != y.path {
return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
}
// 若两个节点的子节点数量不相等 则不相等
nChildrenNum := len(n.children)
yChildrenNum := len(y.children)
if nChildrenNum != yChildrenNum {
return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
}
// 若两个节点的handleFunc类型不同 则不相等
nHandler := reflect.ValueOf(n.HandleFunc)
yHandler := reflect.ValueOf(y.HandleFunc)
if nHandler != yHandler {
return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
}
// 比对两个节点的子节点映射是否相等
for path, child := range n.children {
dstChild, ok := y.children[path]
// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
if !ok {
return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
}
// 比对两个子节点是否相等
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
此处改动了3处:
添加了1个测试用例
wantRouter
中添加了POST方法的路由树wantRouter
中添加了order
节点,并为该节点添加了子节点create
,且为create
子节点添加了HandleFunc
这次是可以成功通过测试的.
1.5 再次为POST方法添加测试用例
这个测试用例是为了测试POST方法中路由仅有1段的case.
添加/login
路由的测试用例:
router_test.go
:
package staticMatchingTestCase
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
method string
path string
}
// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
// step1. 构造路由树
testRoutes := []TestNode{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodPost,
path: "/order/create",
},
{
method: http.MethodPost,
path: "/login",
},
}
r := newRouter()
mockHandleFunc := func(ctx Context) {}
for _, testRoute := range testRoutes {
r.AddRoute(testRoute.method, testRoute.path, mockHandleFunc)
}
// step2. 验证路由树 断言二者是否相等
wantRouter := &router{
trees: map[string]*node{
// GET方法路由树
http.MethodGet: &node{
path: "/",
children: map[string]*node{
"user": {
path: "user",
children: map[string]*node{
"home": &node{
path: "home",
children: nil,
// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
// /user和/ 都是没有handleFunc的
HandleFunc: mockHandleFunc,
},
},
HandleFunc: mockHandleFunc,
},
"order": &node{
path: "order",
children: map[string]*node{
"detail": &node{
path: "detail",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
HandleFunc: mockHandleFunc,
},
// POST方法路由树
http.MethodPost: {
path: "/",
children: map[string]*node{
"order": &node{
path: "order",
children: map[string]*node{
"create": &node{
path: "create",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
"login": &node{
path: "login",
children: nil,
HandleFunc: mockHandleFunc,
},
},
HandleFunc: nil,
},
},
}
// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
// assert.Equal(t, wantRouter, r)
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
// 如果目标路由森林为nil 则不相等
if y == nil {
return fmt.Sprintf("目标路由森林为nil"), false
}
// 如果两个路由森林中的路由树数量不同 则不相等
rTreesNum := len(r.trees)
yTreesNum := len(y.trees)
if rTreesNum != yTreesNum {
return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
}
for method, tree := range r.trees {
dstTree, ok := y.trees[method]
// 如果目标router中没有对应HTTP方法的路由树 则不相等
if !ok {
return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
}
// 比对两棵路由树的结构是否相等
msg, equal := tree.equal(dstTree)
if !equal {
return method + "-" + msg, false
}
}
return "", true
}
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
// 如果目标节点为nil 则不相等
if y == nil {
return fmt.Sprintf("目标节点为nil"), false
}
// 如果两个节点的path不相等 则不相等
if n.path != y.path {
return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
}
// 若两个节点的子节点数量不相等 则不相等
nChildrenNum := len(n.children)
yChildrenNum := len(y.children)
if nChildrenNum != yChildrenNum {
return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
}
// 若两个节点的handleFunc类型不同 则不相等
nHandler := reflect.ValueOf(n.HandleFunc)
yHandler := reflect.ValueOf(y.HandleFunc)
if nHandler != yHandler {
return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
}
// 比对两个节点的子节点映射是否相等
for path, child := range n.children {
dstChild, ok := y.children[path]
// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
if !ok {
return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
}
// 比对两个子节点是否相等
msg, equal := child.equal(dstChild)
if !equal {
return msg, false
}
}
return "", true
}
此处改动了2处:
添加了1个测试用例
wantRouter
中的POST路由树上添加了login
节点,且为该节点添加了HandleFunc
这次是可以成功通过测试的.
PART2. 全静态路由的非法用例
初态工程结构如下:
(base) yanglei@yuanhong 09-illegalTestCase % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go
0 directories, 8 files
2.1 字面量为空字符串的路由
2.1.1 添加对path为空字符串的限制
这里只需要在AddRoute()
方法中限制path
不能为空字符串即可:
router.go
:
package illegalTestCase
import "strings"
// router 路由森林 用于支持对路由树的操作
type router struct {
// trees 路由森林 按HTTP动词组织路由树
// 该map中 key为HTTP动词 value为路由树的根节点
// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
trees map[string]*node
}
// newRouter 创建路由森林
func newRouter() *router {
return &router{
trees: map[string]*node{},
}
}
// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
// step1. 检测路由是否合规
// 1.1 检测路由是否为空字符串
if path == "" {
panic("web: 路由不能为空字符串")
}
// step2. 找到路由树
root, ok := r.trees[method]
// 如果没有找到路由树,则创建一棵路由树
if !ok {
root = &node{
path: "/",
}
r.trees[method] = root
}
// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
if path == "/" {
root.HandleFunc = handleFunc
return
}
// step4. 切割path
// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
// path = path[1:]
path = strings.TrimLeft(path, "/")
segments := strings.Split(path, "/")
// step3. 为路由树添加路由
// Tips: 此处我认为用target指代要添加路由的节点更好理解
target := root