HttpRunner 4.0版本,支持多种用例的编写格式:YAML/JSON/go test/pytest,其中后面两种格式我们都知道通过调用测试函数执行,那YAML/JSON这两种用例格式到底是怎样被运行的呢?下面我们一起分析一下
注意:以下代码被缩略过,只保留核心代码,框架版本:4.3.0
首先先从执行用例时的命令开始
**hrp run case1 case2 **
var runCmd = &cobra.Command{
Use: "run $path...",
Short: "run API test with go engine",
Long: `run yaml/json testcase files for API test`,
Example: ` $ hrp run demo.json # run specified json testcase file
$ hrp run demo.yaml # run specified yaml testcase file
$ hrp run examples/ # run testcases in specified folder`,
Args: cobra.MinimumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) {
setLogLevel(logLevel)
},
// 【重点】:执行命令后调用函数
RunE: func(cmd *cobra.Command, args []string) error {
// 【疑问】:为什么存放用例路径的数组类型是:hrp.ITestCase
var paths []hrp.ITestCase
// 编辑命令参数获取所有待执行用例的路径
for _, arg := range args {
path := hrp.TestCasePath(arg)
paths = append(paths, &path)
}
// 创建运行器
runner := makeHRPRunner()
// 调用执行函数
return runner.Run(paths...)
},
}
总结
在执行命令后,代码处理了
- 创建运行器,根据命令行参数初始化一个运行器
- 调用执行函数,将待执行用例作为参数传入
[疑问]命令行传入的参数是一个用例路径,为什么接收数组类型是: hrp.ITestCase
其实hrp.ITestCase是一个接口,由于框架本身要支持多种用例类型:go test/文件类型/curl... 需要将不同类型的用例转换成一个相同结构在运行,ITestCase 接口就是定义一个规范来实现统一结构。
// 不同的用例格式,只需要实现ITestCase接口定义的两个方法即可通过运行器运行
type ITestCase interface {
GetPath() string
ToTestCase() (*TestCase, error)
}
而在接收到命令行参数后有将参数转换:hrp.TestCasePath(arg)
TestCasePath 是 string 类型的别名,同时实现了ITestCase接口,所以用例路径可以转为:hrp.ITestCase
type TestCasePath string
func (path *TestCasePath) GetPath() string {
return fmt.Sprintf("%v", *path)
}
// ToTestCase loads testcase path and convert to *TestCase
func (path *TestCasePath) ToTestCase() (*TestCase, error) {
tc := &TCase{}
casePath := path.GetPath()
err := builtin.LoadFile(casePath, tc)
if err != nil {
return nil, err
}
return tc.ToTestCase(casePath)
}
执行命令后调用Run方法进行处理
func (r *HRPRunner) Run(testcases ...ITestCase) error {
// ··· 缩略代码
// 初始化执行摘要,用于存储执行结果
s := newOutSummary()
// 【重点】加载测试用例
testCases, err := LoadTestCases(testcases...)
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return err
}
// ··· 缩略代码
var runErr error
// 遍历每一条用例
for _, testcase := range testCases {
// 【重点】每一条用例创建一个独立的运行器
caseRunner, err := r.NewCaseRunner(testcase)
if err != nil {
log.Error().Err(err).Msg("[Run] init case runner failed")
return err
}
// ... 缩略代码
// 【重点】迭代器,负责参数化迭代
// 【疑问】当用例没有参数化时,迭代器会运行吗?
for it := caseRunner.parametersIterator; it.HasNext(); {
// case runner can run multiple times with different parameters
// each run has its own session runner
// 【重点】为每一次参数迭代创建一个会话运行器
sessionRunner := caseRunner.NewSession()
// 【重点】启动会话运行器
err1 := sessionRunner.Start(it.Next())
if err1 != nil {
log.Error().Err(err1).Msg("[Run] run testcase failed")
runErr = err1
}
// 【重点】获取会话的运行结果,
caseSummary, err2 := sessionRunner.GetSummary()
s.appendCaseSummary(caseSummary)
if err2 != nil {
log.Error().Err(err2).Msg("[Run] get summary failed")
if err1 != nil {
runErr = errors.Wrap(err1, err2.Error())
} else {
runErr = err2
}
}
// 运行错误时跳出当前迭代
if runErr != nil && r.failfast {
break
}
}
}
// 获取运行时长
s.Time.Duration = time.Since(s.Time.StartAt).Seconds()
// 【重点】保存测试结果
if r.saveTests {
err := s.genSummary()
if err != nil {
return err
}
}
// 【重点】生成测试报告
if r.genHTMLReport {
err := s.genHTMLReport()
if err != nil {
return err
}
}
return runErr
}
总结
Run方法为实际执行用例的入口
- 加载测试用例统一处理返回 []*TestCase
- 为每一条用例创建一个独立的运行器
- 遍历参数,创建迭代器进行迭代遍历
- 为每次迭代参数创建一个会话运行器
- 启动会话运行器,执行用例
- 采集每个迭代会话的运行结果
- 保存运行结果
- 生成测试报告
[疑问]如果用例中没有设置参数化,迭代器还会运行吗?
答案肯定是会运行,我们在实际使用中肯定遇到过不需要参数化的场景,那在没有参数化时迭代器是怎样执行的呢?
通过分析下面这块代码,发现其实想要执行用例,只需要满足it.HasNext()即可
// 满足:it.HasNext() 即可进入循环
for it := caseRunner.parametersIterator; it.HasNext(); {
// case runner can run multiple times with different parameters
// each run has its own session runner
sessionRunner := caseRunner.NewSession()
err1 := sessionRunner.Start(it.Next())
if err1 != nil {
log.Error().Err(err1).Msg("[Run] run testcase failed")
runErr = err1
}
caseSummary, err2 := sessionRunner.GetSummary()
s.appendCaseSummary(caseSummary)
if err2 != nil {
log.Error().Err(err2).Msg("[Run] get summary failed")
if err1 != nil {
runErr = errors.Wrap(err1, err2.Error())
} else {
runErr = err2
}
}
if runErr != nil && r.failfast {
break
}
}
可以看到只需要满足几个条件,HasNext 将会返回true
- iter.limit == -1
- iter.hasNext == true && iter.index < iter.limit
func (iter *ParametersIterator) HasNext() bool {
if !iter.hasNext {
return false
}
// unlimited mode
if iter.limit == -1 {
return true
}
// reached limit
if iter.index >= iter.limit {
// cache query result
iter.hasNext = false
return false
}
return true
}
想知道上述条件是怎么设置的还要从初始化ParametersIterator开始,通过下面代码分析,在没有设置参数化时初始化代码刚好满足条件2.所以HasNext的判断是可以通过的
func newParametersIterator(parameters map[string]Parameters, config *TParamsConfig) *ParametersIterator {
if config == nil {
config = &TParamsConfig{}
}
// 【重点】初始化 ParametersIterator 此时:hasNext == true index == 0
iterator := &ParametersIterator{
data: parameters,
hasNext: true,
sequentialParameters: nil,
randomParameterNames: nil,
limit: config.Limit,
index: 0,
}
// 【重点】当parameters的长度等于0的时候 limit = 1
if len(parameters) == 0 {
iterator.data = map[string]Parameters{}
iterator.limit = 1
return iterator
}
// ... 省略代码
return iterator
}
此时满足了it.HasNext(),代码继续执行会发现在Start()函数执行的时候,还传入了it.Next()
既然都没有参数化,那it.Next()会发生什么事呢?
func (iter *ParametersIterator) Next() map[string]interface{} {
iter.Lock()
defer iter.Unlock()
if !iter.hasNext {
return nil
}
var selectedParameters map[string]interface{}
// 【重点】初始化时 sequentialParameters 为nil 此时获取长度 == 0 满足条件
if len(iter.sequentialParameters) == 0 {
//【重点】 selectedParameters 初始化为一个空map
selectedParameters = make(map[string]interface{})
} else if iter.index < len(iter.sequentialParameters) {
selectedParameters = iter.sequentialParameters[iter.index]
} else {
// loop back to the first sequential parameter
index := iter.index % len(iter.sequentialParameters)
selectedParameters = iter.sequentialParameters[index]
}
// 【重点】randomParameterNames 初始化时也为nil 此时不进入循环
for _, paramName := range iter.randomParameterNames {
randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
randIndex := randSource.Intn(len(iter.data[paramName]))
for k, v := range iter.data[paramName][randIndex] {
selectedParameters[k] = v
}
}
//【重点】 index ++ 后 保证下次调用it.HasNext() == false
iter.index++
if iter.limit > 0 && iter.index >= iter.limit {
iter.hasNext = false
}
// 【重点】最终会返回一个空map
return selectedParameters
}
版权归原作者 不休不止~ 所有, 如有侵权,请联系我们删除。