
GoConvey是一个完全兼容官方Go Test的测试框架,一般来说这种第三方库都比官方的功能要强大、更加易于使用、开发效率更高.Convey定义了一个局部的作用域,在这个作用域里面我们可以定义变量,调用方法,然后重复继续这个操作,low-level的Convey会继承top-level的变量。还提供了大量增强的Assertions,可以非常方便的对字符串、slice、map结果进行断言测试。框架还提供了一个Web端的UI界面,可以非常方便的查看测试覆盖和运行情况,还可以自动运行测试,执行goconvey命令就可以启动服务。
前面我们介绍gomonkey就用到了goconvey框架,这里我们介绍下它的源码,源码位于:github.com/smartystreets/goconvey
首先看下Convey方法,首先获取当前上下文,如果是空那就新建一个根上下文,否则,继承根上下文,创建一个新的。
func Convey(items ...interface{}) {
if ctx := getCurrentContext(); ctx == nil {
rootConvey(items...)
} else {
ctx.Convey(items...)
}
}func getCurrentContext() *context {
ctx, ok := ctxMgr.GetValue(nodeKey)
if ok {
return ctx.(*context)
}
return nil
} nodeKey = "node"上下文管理器是一个全局变量,它使用了包github.com/jtolds/gls@v4.20.0+incompatible/context.go
var (
ctxMgr *gls.ContextManager本质上就是一个带锁的双层map
type ContextManager struct {
mtx sync.Mutex
values map[uint]Values
}// Values is simply a map of key types to value types. Used by SetValues to
// set multiple values at once.
type Values map[interface{}]interface{}第二层的map key和value都是interface
func (m *ContextManager) GetValue(key interface{}) (
value interface{}, ok bool) {
gid, ok := GetGoroutineId()
if !ok {
return nil, false
}
m.mtx.Lock()
state, found := m.values[gid]
m.mtx.Unlock()
if !found {
return nil, false
}
value, ok = state[key]
return value, ok
}它获取当前goroutine的ID然后通过id定位到map,从map里通过key获取值
func GetGoroutineId() (gid uint, ok bool) {
return readStackTag()
}func readStackTag() (tag uint, ok bool) {
var current_tag uint
offset := 0
for {
batch, next_offset := getStack(offset, stackBatchSize)
for _, pc := range batch {
val, ok := pc_lookup[pc]
if !ok {
continue
}
if val < 0 {
return current_tag, true
}
current_tag <<= bitWidth
current_tag += uint(val)
}
if next_offset == 0 {
break
}
offset = next_offset
}
return 0, false
} getStack = func(offset, amount int) (stack []uintptr, next_offset int) {
stack = make([]uintptr, amount)
stack = stack[:runtime.Callers(offset, stack)]
if len(stack) < amount {
return stack, 0
}
return stack, offset + len(stack)
}然后看看rootConvey,它首先解析入参,让后校验是否传入了Test参数,接着用解析道的参数构建了一个context,最后用context作为key,访问用例的迭代方法作为value把它存储到tls存储,也就是前面介绍的map。
func rootConvey(items ...interface{}) {
entry := discover(items)
if entry.Test == nil {
conveyPanic(missingGoTest)
}
expectChildRun := true
ctx := &context{
reporter: buildReporter(),
children: make(map[string]*context),
expectChildRun: &expectChildRun,
focus: entry.Focus,
failureMode: defaultFailureMode.combine(entry.FailMode),
stackMode: defaultStackMode.combine(entry.StackMode),
}
ctxMgr.SetValues(gls.Values{nodeKey: ctx}, func() {
ctx.reporter.BeginStory(reporting.NewStoryReport(entry.Test))
defer ctx.reporter.EndStory()
for ctx.shouldVisit() {
ctx.conveyInner(entry.Situation, entry.Func)
expectChildRun = true
}
})
}接下来我们看看解析的过程
func discover(items []interface{}) *suite {
name, items := parseName(items)
test, items := parseGoTest(items)
failure, items := parseFailureMode(items)
stack, items := parseStackMode(items)
action, items := parseAction(items)
specifier, items := parseSpecifier(items)
if len(items) != 0 {
conveyPanic(parseError)
}
return newSuite(name, failure, stack, action, test, specifier)
}其实是一个出栈的过程,先解析名字,如果失败就panic
func parseName(items []interface{}) (string, []interface{}) {
if name, parsed := item(items).(string); parsed {
return name, items[1:]
}
conveyPanic(parseError)
panic("never get here")
}然后解析testing.T参数
func parseGoTest(items []interface{}) (t, []interface{}) {
if test, parsed := item(items).(t); parsed {
return test, items[1:]
}
return nil, items
}然后是参数FailureMode,是So模块失败后的返回类型
func parseFailureMode(items []interface{}) (FailureMode, []interface{}) {
if mode, parsed := item(items).(FailureMode); parsed {
return mode, items[1:]
}
return FailureInherits, items
}然后是StackMode,So模块成功后的返回
func parseStackMode(items []interface{}) (StackMode, []interface{}) {
if mode, parsed := item(items).(StackMode); parsed {
return mode, items[1:]
}
return StackInherits, items
}然后是解析函数
func parseAction(items []interface{}) (func(C), []interface{}) {
switch x := item(items).(type) {
case nil:
return nil, items[1:]
case func(C):
return x, items[1:]
case func():
return func(C) { x() }, items[1:]
}
conveyPanic(parseError)
panic("never get here")
}他的参数是一个接口,如果没有传,默认也是一个接口
type C interface {
Convey(items ...interface{})
SkipConvey(items ...interface{})
FocusConvey(items ...interface{})
So(actual interface{}, assert Assertion, expected ...interface{})
SoMsg(msg string, actual interface{}, assert Assertion, expected ...interface{})
SkipSo(stuff ...interface{})
Reset(action func())
Println(items ...interface{}) (int, error)
Print(items ...interface{}) (int, error)
Printf(format string, items ...interface{}) (int, error)
}最后是解析actionSpecifier
func parseSpecifier(items []interface{}) (actionSpecifier, []interface{}) {
if len(items) == 0 {
return noSpecifier, items
}
if spec, ok := items[0].(actionSpecifier); ok {
return spec, items[1:]
}
conveyPanic(parseError)
panic("never get here")
}解析完这6个参数后,如果队列里还有值就panic,最后创建一个suit,把这些参数放到结构体里
func newSuite(situation string, failureMode FailureMode, stackMode StackMode, f func(C), test t, specifier actionSpecifier) *suite {
ret := &suite{
Situation: situation,
Test: test,
Func: f,
FailMode: failureMode,
StackMode: stackMode,
}
switch specifier {
case skipConvey:
ret.Func = nil
case focusConvey:
ret.Focus = true
}
return ret
}接着我们看看注册的值里面的迭代函数,首先判断是否完成,或者是否希望子节点继续调用。
func (c *context) shouldVisit() bool {
return !c.complete && *c.expectChildRun
}然后是执行方法
func (ctx *context) conveyInner(situation string, f func(C)) {
// Record/Reset state for next time.
defer func() {
ctx.executedOnce = true
// This is only needed at the leaves, but there's no harm in also setting it
// when returning from branch Convey's
*ctx.expectChildRun = false
}()
// Set up+tear down our scope for the reporter
ctx.reporter.Enter(reporting.NewScopeReport(situation))
defer ctx.reporter.Exit()
// Recover from any panics in f, and assign the `complete` status for this
// node of the tree.
defer func() {
ctx.complete = true
if problem := recover(); problem != nil {
if problem, ok := problem.(*conveyErr); ok {
panic(problem)
}
if problem != failureHalt {
ctx.reporter.Report(reporting.NewErrorReport(problem))
}
} else {
for _, child := range ctx.children {
if !child.complete {
ctx.complete = false
return
}
}
}
}()
// Resets are registered as the `f` function executes, so nil them here.
// All resets are run in registration order (FIFO).
ctx.resets = []func(){}
defer func() {
for _, r := range ctx.resets {
// panics handled by the previous defer
r()
}
}()
if f == nil {
// if f is nil, this was either a Convey(..., nil), or a SkipConvey
ctx.reporter.Report(reporting.NewSkipReport())
} else {
f(ctx)
}
}通过一系列defer函数实现后进先出,首先调用当前函数,然后是执行rest函数列表,接着进行revovery,如果没有panic就判断当前节点的孩子是否有未完成的,如果有,就把当前contetxt设置成未完成,接着退出上报,最后设置expectChildRun和executedOnce
本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!