Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

goconvey的用途和实现 #65

Open
Petelin opened this issue Jun 29, 2021 · 0 comments
Open

goconvey的用途和实现 #65

Petelin opened this issue Jun 29, 2021 · 0 comments

Comments

@Petelin
Copy link
Owner

Petelin commented Jun 29, 2021

goconvey的用途和实现

Go 语言官方test工具已经非常令人满意了,使用表格法可以很好的做单元测试。但是在做业务逻辑上的组合测试(也不是集成测试,不知道有没有名次描述这个)的时候就有点麻烦,比如我们要测试1v1呼叫后的几个action有没有问题。可以想到这么几条测试案例

  • A呼叫B
    • B拒绝
    • B接受
    • A取消

这里我们真正要测试的操作都必须有一个【业务前提】:A呼叫了B。 (这里强调业务前提是这里介绍的不是tear up和tear down那种测试方法)

我们可以这么写

func TestTestSingleCall1(t *testing.T) {
	var createSingleCall func() interface{} //TODO

	tcases := []struct {
		desc string
		do   func(interface{}) error
		err  error
	}{
		{
			"accept",
			func(i interface{}) error {
				// do accpet
				return nil
			},
			nil,
		},
		{
			"reject",
			func(i interface{}) error {
				// do accpet
				return nil
			},
			nil,
		},
	}

	for _, tcase := range tcases {
		err := tcase.do(createSingleCall())
		assert.Equal(t, tcase.err, err)
	}
}

这样写总比写三次create single call好一点,但是如果想要多层嵌套呢?如果create single call返回了更多的参数呢?打表法更适合简单的参数组合多样的测试,在这种场景下就不是很好用了。

这里介绍一个神器 goconvey: 不仅仅可以解决上面重复创建1v1通话的问题,还有更好的UI,WEB界面,断言方法等等......
不过这篇文档只是讲解go convey的测试逻辑。

go convey官方demo如下

func TestIntegerStuff(t *testing.T) {
	Convey("Given some integer with a starting value", t, func() {
		x := 1

		Convey("When the integer is incremented", func() {
			x++

			Convey("The value should be greater by two", func() {
				x++
				So(x, ShouldEqual, 3)
			})
			
			Convey("The value should be greater by one", func() {
				So(x, ShouldEqual, 2)
			})
		})
	})
}

注意两个So语句的不同,第二个So并没有受到上面一个Convey的影响!也就是说测试总是从Root结点出发,一直跑到某一个叶子结点,有多少个叶子结点就会重复跑Root结点多少次。这里可以看上面的测试案例,仔细体会一下

我们上面说的业务场景的测试就可以改成这样

func TestTestSingleCall2(t *testing.T) {
	var createSingleCall func() interface{}

	RootConvey("A Call B", t, func(t *testing.T) {
		x := createSingleCall()

		Convey("B accept", func(t *testing.T) {
			//x 是被闭包的对象可以直接用
			_ = x
		})

		Convey("B Reject", func(t *testing.T) {
			Convey("Reject-result", func(t *testing.T) {
				//So() 拒绝的结果在这里判断
			})
			Convey("A Call B again", func(t *testing.T) {
				//So() 再一次呼叫依然能成功
			})
		})
	})
}

这样测试逻辑清晰多了吧。而且create single call也只写了一次,测了三个场景。

  • Call - accept
  • Call - reject
  • Call - reject - Call

实现

goconvey的实现非常简单,核心实现只需要100行,和一个叫gls的context包,这个包也非常神奇,有兴趣的自己看下这个包。

package testpackage

import (
	"testing"

	"github.com/jtolds/gls"
)

var (
	ctxMgr  = gls.NewContextManager()
	nodeKey = "__p__"
)

func getCtx() *Stage {
	ctx, ok := ctxMgr.GetValue(nodeKey)
	if ok {
		return ctx.(*Stage)
	}
	return nil
}

type Stage struct {
	desc string
	t    *testing.T

	children map[string]*Stage

	complete     bool  // 这个方法和所有的子方法都执行完了
	canChildRun  *bool // 能不能进去run方法
	executedOnce bool  // 为了校验
}

func (b *Stage) Run(fn func(t *testing.T)) {
	defer func() {
		b.executedOnce = true
		if b.allChildrenDone() {
			b.complete = true
		}
		*b.canChildRun = false
	}()
	fn(b.t)
}

func (b *Stage) allChildrenDone() bool {
	isDone := true
	for _, b := range b.children {
		if !b.complete {
			isDone = false
		}
	}
	return isDone
}

func (b *Stage) shouldVisit() bool {
	// 没完成,并且有资格执行子方法
	return !b.complete && *b.canChildRun
}

func RootConvey(desc string, t *testing.T, fn func(t2 *testing.T)) {
	expectChildRun := true
	b := &Stage{
		desc:         desc,
		t:            t,
		children:     make(map[string]*Stage),
		complete:     false,
		canChildRun:  &expectChildRun,
		executedOnce: false,
	}
	ctxMgr.SetValues(gls.Values{nodeKey: b}, func() {
		for b.shouldVisit() {
			b.Run(fn)
			expectChildRun = true
		}
	})
}

func Convey(desc string, fn func(t *testing.T)) {
	parent := getCtx()

	var innerBean *Stage

	if parent.executedOnce {
		innerBean = parent.children[desc] //must success
	} else {
		innerBean = &Stage{
			t:           parent.t,
			desc:        desc,
			children:    make(map[string]*Stage),
			complete:    false,
			canChildRun: parent.canChildRun,
		}
		parent.children[desc] = innerBean
	}

	if innerBean.shouldVisit() {
		ctxMgr.SetValues(gls.Values{nodeKey: innerBean}, func() {
			innerBean.Run(fn)
		})
	}
}

实现上比较trace的点

  • 利用了函数栈
  • canChildRun是一个指针,只要有一个叶子结点函数被调用过了,那么就会变成false,函数栈一路返回直至Root函数
  • Root函数会一直循环调用自己,直到子结点都complet了,只有Root函数会/有权利设置canChildRun,所以每一个叶子结点的执行,都是从Root开始的~
  • Run函数接受一个方法,这个方法可以闭包的包含一些变量,这个函数指针不能保存, 否则闭包的非指针变量就再也没办法被外界刷新了
  • ctxMgr是一个神器的全局变量并且是跟栈绑定的,在内部函数set进去一个值,如果函数返回了,外面就拿不到set的值了
  • executedOnce 没太多用处,如果一个Convey返回了,那么executeOnce一定是True,并且他的所有子结点,一定已经被遍历过了,但是子结点的子结点不一定被遍历过了--这要看complete变量
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant