首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >golang源码分析:goconvey(1)

golang源码分析:goconvey(1)

作者头像
golangLeetcode
发布2026-03-18 18:16:16
发布2026-03-18 18:16:16
520
举报

GoConvey是一个完全兼容官方Go Test的测试框架,一般来说这种第三方库都比官方的功能要强大、更加易于使用、开发效率更高.Convey定义了一个局部的作用域,在这个作用域里面我们可以定义变量,调用方法,然后重复继续这个操作,low-level的Convey会继承top-level的变量。还提供了大量增强的Assertions,可以非常方便的对字符串、slice、map结果进行断言测试。框架还提供了一个Web端的UI界面,可以非常方便的查看测试覆盖和运行情况,还可以自动运行测试,执行goconvey命令就可以启动服务。

前面我们介绍gomonkey就用到了goconvey框架,这里我们介绍下它的源码,源码位于:github.com/smartystreets/goconvey

首先看下Convey方法,首先获取当前上下文,如果是空那就新建一个根上下文,否则,继承根上下文,创建一个新的。

代码语言:javascript
复制
func Convey(items ...interface{}) {
    if ctx := getCurrentContext(); ctx == nil {
        rootConvey(items...)
    } else {
        ctx.Convey(items...)
    }
}
代码语言:javascript
复制
func getCurrentContext() *context {
    ctx, ok := ctxMgr.GetValue(nodeKey)
    if ok {
        return ctx.(*context)
    }
    return nil
}
代码语言:javascript
复制
    nodeKey = "node"

上下文管理器是一个全局变量,它使用了包github.com/jtolds/gls@v4.20.0+incompatible/context.go

代码语言:javascript
复制
var (
    ctxMgr *gls.ContextManager

本质上就是一个带锁的双层map

代码语言:javascript
复制
type ContextManager struct {
    mtx    sync.Mutex
    values map[uint]Values
}
代码语言:javascript
复制
// 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

代码语言:javascript
复制
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获取值

代码语言:javascript
复制
func GetGoroutineId() (gid uint, ok bool) {
    return readStackTag()
}
代码语言:javascript
复制
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
}
代码语言:javascript
复制
    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。

代码语言:javascript
复制
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
        }
    })
}

接下来我们看看解析的过程

代码语言:javascript
复制
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

代码语言:javascript
复制
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参数

代码语言:javascript
复制
func parseGoTest(items []interface{}) (t, []interface{}) {
    if test, parsed := item(items).(t); parsed {
        return test, items[1:]
    }
    return nil, items
}

然后是参数FailureMode,是So模块失败后的返回类型

代码语言:javascript
复制
func parseFailureMode(items []interface{}) (FailureMode, []interface{}) {
    if mode, parsed := item(items).(FailureMode); parsed {
        return mode, items[1:]
    }
    return FailureInherits, items
}

然后是StackMode,So模块成功后的返回

代码语言:javascript
复制
func parseStackMode(items []interface{}) (StackMode, []interface{}) {
    if mode, parsed := item(items).(StackMode); parsed {
        return mode, items[1:]
    }
    return StackInherits, items
}

然后是解析函数

代码语言:javascript
复制
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")
}

他的参数是一个接口,如果没有传,默认也是一个接口

代码语言:javascript
复制
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

代码语言:javascript
复制
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,把这些参数放到结构体里

代码语言:javascript
复制
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
}

接着我们看看注册的值里面的迭代函数,首先判断是否完成,或者是否希望子节点继续调用。

代码语言:javascript
复制
func (c *context) shouldVisit() bool {
    return !c.complete && *c.expectChildRun
}

然后是执行方法

代码语言:javascript
复制
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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-06-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档