HttpRunnerv4一条用例是怎么被执行的-创新互联-成都快上网建站

HttpRunnerv4一条用例是怎么被执行的-创新互联

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

创新互联公司自2013年创立以来,先为魏都等服务建站,魏都等地企业,进行企业商务咨询服务。为魏都企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。

注意:以下代码被缩略过,只保留核心代码,框架版本: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
}

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


文章名称:HttpRunnerv4一条用例是怎么被执行的-创新互联
网站路径:http://kswjz.com/article/cdgdoi.html
扫二维码与项目经理沟通

我们在微信上24小时期待你的声音

解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流