HttpRunner v4 一条用例是怎么被执行的

2023-10-26

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...)
	},
}

总结

在执行命令后,代码处理了

  1. 创建运行器,根据命令行参数初始化一个运行器

  2. 调用执行函数,将待执行用例作为参数传入

[疑问]命令行传入的参数是一个用例路径,为什么接收数组类型是: 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方法为实际执行用例的入口

  1. 加载测试用例统一处理返回 []*TestCase
  2. 为每一条用例创建一个独立的运行器
  3. 遍历参数,创建迭代器进行迭代遍历
  4. 为每次迭代参数创建一个会话运行器
  5. 启动会话运行器,执行用例
  6. 采集每个迭代会话的运行结果
  7. 保存运行结果
  8. 生成测试报告

[疑问]如果用例中没有设置参数化,迭代器还会运行吗?

答案肯定是会运行,我们在实际使用中肯定遇到过不需要参数化的场景,那在没有参数化时迭代器是怎样执行的呢?

通过分析下面这块代码,发现其实想要执行用例,只需要满足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

  1. iter.limit == -1
  2. 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
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

HttpRunner v4 一条用例是怎么被执行的 的相关文章

随机推荐

  • 应用程序无法正常启动(0x000007b)或者找不到dll文件(以vcruntime140d.dll为例)的原因原理分析和解决方法(亲测已解决)

    文章目录 一 问题1 由于找不到vcruntime140d dll 无法继续执行代码 重新安装程序可能会解决此问题 二 问题2 应用程序无法正常启动 0x000007b 请单击 确定 关闭应用程序 三 Windows目录下的SysWOW64
  • 【开始学习React Hook(1)】Hook之useState

    react hook是react推出的一种特殊函数 这些函数可以让你在不创建react class的情况下依然可以使用react的一些特性 诸如目前react的钩子函数拥有的所有特性 最常用的hook有useState useEffect
  • 网络安全——从新手入门到大师的学习攻略

    最近经常有网友问我网络安全方面怎么样从入门到大师 或者成为黑客 红客等 其实我想说的是 只要你有毅力 能坚持学习各种必须的网络安全技术和理论知识 随着时间的积累 最终一定会成为你心中最想成为的网络安全专家 本文就着重讲解下网络安全从入门到大
  • Docker搭建漏洞靶场(Vulhub、Vulnapp、Vulfocus)

    文章目录 vulhub 靶场搭建 简介 环境搭建过程 vulnapp靶场搭建 vulfocus靶场搭建 简介 环境搭建 vulhub 靶场搭建 简介 Vulhub是一个面向大众的开源漏洞靶场 无需docker知识 简单执行一条命令即可编译
  • 数据中心联盟第五批大数据产品评测结果出炉,腾讯云大数据斩获多个奖项

    欢迎大家前往腾讯云社区 获取更多腾讯海量技术实践干货哦 近日 在数据中心联盟组织的第五批大数据产品评测中 腾讯云大数据平台取得了两项第一名 特别在Hbase性能上有非常亮眼的表现 其他各项成绩也名列前茅 本月7日 中国通信标准化协会常务副秘
  • 以太坊学习-笔记1

    加密 以太坊有两种不同类型的账户 外部拥有账户 EOA 和合约 EOA 的以太坊地址是从密钥对的公钥部分生成的 以太坊使用的系统是基于公钥加密的系统 密钥是成对出现的 由一个私有 秘密 密钥和一个公共密钥组成 私钥提供对账户的控制权 公钥将
  • Java加密之IV

    AES是一种 分组密码 密码学中 分组 block 密码的工作模式 mode of operation 允许使用同一个分组密码密钥对多于一块的数据进行加密 并保证其安全性 分组密码自身只能加密长度等于密码分组长度的单块数据 若要加密变长数据
  • LeetCode 127. 单词接龙(C++)*

    思路 1 如果采用回溯法来的话会超时 2 这里采用构造图和广度优先遍历结合来实现 首先要构造图 需要将每个字符串对应一个数字id 然后边的构造使用矩阵来实现 这里采用将每一个字符串的id连接每个将该字符串的其中一个字符改为未知字符的字符串的
  • Python ffmpeg视频处理

    2 源码 coding utf 8 import ffmpeg import getpass import subprocess import matplotlib pyplot as plt import cv2 import numpy
  • android studio(自带SDK)安装教程

    下载网址 http www android studio org index php download hisversion 当下载为 exe程序时 直接双击 exe程序 之后点击Next 第一地址是安装路径 自己选择即可 第二个是SDK路
  • 手把手教你搭建Windows环境微信小程序的本地测试服务器

    Mac环境下 手把手教你搭建Mac环境微信小程序的本地测试服务器 问题的提出 Windows环境 方便快捷地搭建小程序的测试服务器 小程序对于网络请求的URL的特殊要求 不能出现端口号 不能用localhost 必须用https 主要步骤
  • elementUI侧边栏实现响应式,响应式侧边栏

    实现思路 准备两份aside侧边栏 借助display和媒体查询实现响应式 以下是完整代码
  • 初次使用vite新建项目报错

    第一次使用vite新建项目时报错 C Users AppData Roaming npm cache npx 13100 node modules create vite index js 4 import fs from node fs
  • 测试报告(进阶)

    功能测试可以手写一份测试报告 一 如何自动生成测试报告 unittest生成测试报告 测试用例 账号正确 密码错误 自己依照测试用例输入一组账号 点登录 会出现信息 密码错误 代码 import unittest import time f
  • vue 监听div宽高变化

    npm install element resize detector save import elementResizeDetectorMaker from element resize detector mounted const th
  • 如何使用chatglm-6b实现多卡训练

    首先先说ChatGLM 6b是支持多卡训练的 步骤如下 1 安装 NVIDIA CUDA Toolkit 要使用多卡训练 需要安装 CUDA Toolkit 可以在 NVIDIA 官网下载适用于操作系统的 CUDA 版本 2 确认所有的显卡
  • 实验四 MGRE与OSPF综合实验

    1 R6为ISP只能配置IP地址 R1 R5的环回为私有网段 2 R1 4 5为全连的MGRE结构 R1 2 3的星型的拓扑结构 R1为中心站点 3 所有私有网段可以互相通讯 私有网段使用OSPF完成 新建拓扑图 配置合理的IP R1 R2
  • 解决IDEA、PyCharm、PhpStorm及Android Studio中输入法卡住、光标不跟随的问题

    2017新版JetBrains全家桶下的各个软件都存在使用中文输入法时出现类似卡住 即光标不跟随的现象 解决办法 删除软件所在根目录下的jre或jre64文件夹 删除后软件会自动使用本机的jre 并可能提示jie已不是最新版 但不影响使用
  • 详解Vector

    目录 一 Vector介绍 二 源码解析 1 Vector实现的接口 2 Vector的构造方法 1 无参构造方法 2 带初始容量的构造方法 3 带初始容量和增量的构造方法 4 集合型构造方法 3 Vector中的变量 4 Vector主要
  • HttpRunner v4 一条用例是怎么被执行的

    HttpRunner 4 0版本 支持多种用例的编写格式 YAML JSON go test pytest 其中后面两种格式我们都知道通过调用测试函数执行 那YAML JSON这两种用例格式到底是怎样被运行的呢 下面我们一起分析一下 注意