问题起源

问题起源于项目中单测代码中多次调用了sqlmock代码导致结果出现问题,觉得Testing中的并发可能是问题诱因,后来通过看源码发现所用的方式为串行执行方式,后经过实验为对sqlmock的多次同一sql语句的mock导致结果匹配出现问题。


for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {c := &Config{ID:        tt.fields.ID,Label:     tt.fields.Label,ToolID:    tt.fields.ToolID,Cmd:       tt.fields.Cmd,Target:    tt.fields.Target,Scope:     tt.fields.Scope,Action:    tt.fields.Action,CreatedAt: tt.fields.CreatedAt,UpdatedAt: tt.fields.UpdatedAt,}patches, patchesin := tt.mock()got, err := c.Insert(tt.args.ctx, tt.args.matcher)if (err != nil) != tt.wantErr {t.Errorf("Insert() error = %v, wantErr %v", err, tt.wantErr)return}if got != tt.want {t.Errorf("Insert() got = %v, want %v", got, tt.want)}patches.Reset()patchesin.Reset()})
}

该方式为我们在项目中可以通过golang插件直接生成的Test代码,也是我们经常会使用的调用方式。项目中我们使用了gomokey和sqlmock的框架,为了我们的单测的可读性和逻辑性更强我们使用mock函数对所用的代码进行封装,对所用的测试用例我们只需要修改其中的值,就可以满足多个单测需求。

mock: func() (*gomonkey.Patches, *gomonkey.Patches) {sqlMock.ExpectBegin()sqlMock.ExpectExec("INSERT INTO `config`").WillReturnResult(sqlmock.NewResult(1, 1))sqlMock.ExpectCommit()outputs := []gomonkey.OutputCell{{Values: gomonkey.Params{&Matcher{}}},}patches := gomonkey.ApplyFuncSeq(GetMatcherHandle, outputs)outputsin := []gomonkey.OutputCell{{Values: gomonkey.Params{uint(123), nil}},}patchesin := gomonkey.ApplyMethodSeq(reflect.TypeOf(&Matcher{}), "Insert", outputsin)return patches, patchesin},

由于上面的代码分装方式,结果我们在多次test之间对sqlMock的相同insert语句进行了不同结果的mock,导致最后的sql语句匹配出现问题,刚开始考虑是Testing的并发导致相关问题,冲突代码如下:

 sqlMock.ExpectBegin()sqlMock.ExpectExec("INSERT INTO `config`").WillReturnResult(sqlmock.NewResult(1, 1))sqlMock.ExpectCommit()

Testing源码

T结构重组

 atomic.StoreInt32(&t.hasSub, 1)//原子性操作testName, ok, _ := t.context.match.fullName(&t.common, name)if !ok || shouldFailFast() {return true}// Record the stack trace at the point of this call so that if the subtest// function - which runs in a separate stack - is marked as a helper, we can// continue walking the stack into the parent test.  记录堆栈,为了子堆栈可以返回到父堆栈var pc [maxStackLen]uintptrn := runtime.Callers(2, pc[:])t = &T{common: common{barrier: make(chan bool),//父子通信信道signal:  make(chan bool),//完成通信信道name:    testName,parent:  &t.common,level:   t.level + 1,creator: pc[:n],chatty:  t.chatty,//同步输出print},context: t.context,}t.w = indenter{&t.common}

首先进入Testing的Run函数之后我们可以发现,比较关键的是run首先对数据进行了处理,也就是得到testName,接下来比较关键的就是,会重新生成一个新的T,根据之前我们叫它子T,根据父T即现在的t赋值,同时将父T引用赋值到我们子T的parent值中,形成我们需要的T并取地址赋值给原来的t,最后的结果就是这些T会形成如下的一个链表

其中有比较关键的两个信道变量barrier和signal,barrier在后面主要负责父test对子test的消息传递,也就是父子go程之间的通信,而signal则负责对run go程和tRunner go程的通信。其中还有chatty变量,该变量是chattyPrinter指针类型,我的理解就是满足我们的同步输出,最后将我们的输出一起进行打印

go程通信

    // Instead of reducing the running count of this test before calling the// tRunner and increasing it afterwards, we rely on tRunner keeping the// count correct. This ensures that a sequence of sequential tests runs// without being preempted, even when their parent is a parallel test. This// may especially reduce surprises if *parallel == 1. 取代运行数量调用之前减少之后增加,用tRunner保证。保证串行不会被强占,即使父任务是并行go tRunner(t, f)if !<-t.signal {//终止信号 FailNow被调用// At this point, it is likely that FailNow was called on one of the// parent tests by one of the subtests. Continue aborting up the chain.runtime.Goexit()}return !t.failed

通过源码我们可以发现,其实如果只是对t的多次调用我们是通过signal这个信道进行通信,也就是在tRnner go程中对signal进行发送,在这里进行接收我们就可以继续程序的执行,否则我们程序就阻塞在该处等待,也就是说如果我们不调用t的Parallel函数,其实整个的执行流程就和串行是一样的效果

t.report() // Report after all subtests have finished.所有子任务结束// Do not lock t.done to allow race detector to detect race in case// the user does not appropriately synchronizes a goroutine.  不能lockt.done = true  //任务结束if t.parent != nil && atomic.LoadInt32(&t.hasSub) == 0 {t.setRan()}t.signal <- signal  //子failnow且有完成的父test停止

当我们完成我们的f函数执行之后,go程进行report调用,也就是对结果进行报告之后就会对信道signal进行发送,传入信号,接下来在Run中的go程阻塞处就会收到我们发送的值,要么进行退出,要么进行返回,也就是整个逻辑流程和串行的结果是相同的。

Testing并发

要是我们只要如此进行test的串行调用,使用signal信道进行同步其实就没有什么意义了,其实在Testing中是可以进行Testing的并发的,我们来看一下google官方给的示例:

func TestGroupedParallel(t *testing.T) {for _, tc := range tests {tc := tc // capture range variablet.Run(tc.Name, func(t *testing.T) {t.Parallel()...})}
}

Parallel
我们看到示例中调用了Parallel这个函数,这个就是我们实现并发的关键,我们来看Parallel中的源码:

// Add to the list of tests to be released by the parent.  加到父亲的test里t.parent.sub = append(t.parent.sub, t)t.raceErrors += race.Errors()if t.chatty != nil {// Unfortunately, even though PAUSE indicates that the named test is *no// longer* running, cmd/test2json interprets it as changing the active test// for the purpose of log parsing. We could fix cmd/test2json, but that// won't fix existing deployments of third-party tools that already shell// out to older builds of cmd/test2json — so merely fixing cmd/test2json// isn't enough for now.t.chatty.Updatef(t.name, "=== PAUSE %s\n", t.name)}t.signal <- true   // Release calling test. t这个任务结束<-t.parent.barrier // Wait for the parent test to complete.  等待父test完成t.context.waitParallel()//增加并行数量

源码中比较关键的就是将子test加到了parent中的sub里,也就是在接下来的执行中我们的父test是需要等待所有的子test执行完的,其中完成并发的关键就是我们将signal这个信道提前发送,也就是我们在这里Run go程中就会接收到我们的信号,执行完毕,开启下一个go程的创建,从而实现我们的多go程的创建,最后实现我们的并发。

父子test的通信

 if len(t.sub) > 0 {//父任务// Run parallel subtests.完成并行子任务// Decrease the running count for this test.t.context.release()//该任务完成释放该任务数量// Release the parallel subtests.close(t.barrier)//关闭信道// Wait for subtests to complete.for _, sub := range t.sub {<-sub.signal}//阻塞等待子go程信号cleanupStart := time.Now()err := t.runCleanup(recoverAndReturnPanic)t.duration += time.Since(cleanupStart)if err != nil {doPanic(err)}if !t.isParallel {// Reacquire the count for sequential tests. See comment in Run. 再次获取顺序任务t.context.waitParallel()//增加任务数量,可以开始并行}} else if t.isParallel {//最后一个并行任务// Only release the count for this test if it was run as a parallel  只释放这个test的数量// test. See comment in Run method.t.context.release() }

我们可以看到当子test发送signal消息之后就会进行阻塞等待我们的barrier信道,父test执行完之后就会调用release之后就会发送我们的barrier信号,收到信号之后子test的tRnner go程就会在barrier的阻塞处继续执行,释放我们的并发子test。

然后我们可以看到父go程也开开始阻塞等待子test的signal完成信号,也就是说父test必须等待所有子test完成之后才会进行接下来的运行。

这些大概就是我理解的在Testing中的并发执行的流程,但是自己没有把所有代码都看了,还有很多没有兼顾到,难免会有错误,希望大家能够不吝赐教。

【GO】GO Testing源码学习相关推荐

  1. Java多线程之JUC包:Semaphore源码学习笔记

    若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...

  2. JDK11源码学习05 | HashMap类

    JDK11源码学习05 | HashMap类 JDK11源码学习01 | Map接口 JDK11源码学习02 | AbstractMap抽象类 JDK11源码学习03 | Serializable接口 ...

  3. webrtc源码学习 - 点对点(P2P)链接过程(peer connection)

    创建PC pc 是 peer connection 的简写,以下文章中pc 都特指 peer connection PeerConnection 是webrtc 中链接过程非常重要的接口,提供了包括, ...

  4. Shiro源码学习之二

    接上一篇 Shiro源码学习之一 3.subject.login 进入login public void login(AuthenticationToken token) throws Authent ...

  5. Shiro源码学习之一

    一.最基本的使用 1.Maven依赖 <dependency><groupId>org.apache.shiro</groupId><artifactId&g ...

  6. mutations vuex 调用_Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)...

    前言 Vuex源码系列不知不觉已经到了第六篇.前置的五篇分别如下: 长篇连载:Vuex源码学习(一)功能梳理 长篇连载:Vuex源码学习(二)脉络梳理 作为一个Web前端,你知道Vuex的instal ...

  7. vue实例没有挂载到html上,vue 源码学习 - 实例挂载

    前言 在学习vue源码之前需要先了解源码目录设计(了解各个模块的功能)丶Flow语法. src ├── compiler # 把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能. ├── ...

  8. 2021-03-19Tomcat源码学习--WebAppClassLoader类加载机制

    Tomcat源码学习--WebAppClassLoader类加载机制 在WebappClassLoaderBase中重写了ClassLoader的loadClass方法,在这个实现方法中我们可以一窥t ...

  9. jQuery源码学习之Callbacks

    jQuery源码学习之Callbacks jQuery的ajax.deferred通过回调实现异步,其实现核心是Callbacks. 使用方法 使用首先要先新建一个实例对象.创建时可以传入参数flag ...

最新文章

  1. 路由器上实现DHCP和DHCP中继
  2. 让php4和php5共存的方法
  3. visualstudio调试html,Visual Studio Code中调试JavaScript
  4. NCC Meetup 2018 Shanghai 活动小结
  5. html-表单的应用
  6. 第十八节:教你如何使用ES6的Promise对象
  7. Open cup #2
  8. Oracle在Solaris下的机能与调整简介
  9. 分享一个百度云加速下载工具
  10. 桥连模式,模板模式的改进
  11. 《给李彦宏先生的一封信》
  12. 光明乳业孤独症暖心礼包,让“星星的孩子”遇见光明未来
  13. net start mysql:无法启动
  14. 一个Java 程序的主方法_java application程序中,每一个类中,必有一个主方法main()方法。...
  15. 替代A4988的微型打印机驱动TMI8421国产电机驱动芯片
  16. 机器学习-47-ML-03-Metric-based Approach Train+Test as RNN(元学习-support set和query set用于同一网络的方法)
  17. 利用dlib81人脸关键点提取额头脸颊ROI
  18. java数据结构红黑树上旋下旋_存储系统的基本数据结构之一: 跳表 (SkipList)
  19. 八、T100应付管理系统之员工费用报销管理篇
  20. 【赛题回顾】2019 年海淀区中小学生信息学奥林匹克竞赛小学组真题

热门文章

  1. android资料转移到iphone,怎么将安卓手机数据资料转到iPhone上
  2. Java(2): java for循环遍历数组
  3. Android MediaScannerConnection扫描文件
  4. wps正则(通配符)替换【简要版】
  5. LayUI 数据表格 table表格在同一单元格换行显示2个数据 一个单元格放2个数据或多个数据
  6. 一文读懂索引的基本原理!
  7. 电商零售数仓建模之用户01:用户业务模型
  8. 体重 年龄 性别 身高 预测鞋码_儿童标准身高体重、脚长对照表-儿童身高鞋码...
  9. SSD、HDD、SSHD是什么硬盘
  10. 计算机办公店,办公用品和电脑数码店面装修效果图 2016办公文具店门面及室内布置摆放设计图...