HttpRunner 4.0版本,支持多種用例的編寫格式:YAML/JSON/go test/pytest,其中后面兩種格式我們都知道通過調(diào)用測試函數(shù)執(zhí)行,那YAML/JSON這兩種用例格式到底是怎樣被運行的呢?下面我們一起分析一下
創(chuàng)新互聯(lián)公司自2013年創(chuàng)立以來,先為魏都等服務(wù)建站,魏都等地企業(yè),進行企業(yè)商務(wù)咨詢服務(wù)。為魏都企業(yè)網(wǎng)站制作PC+手機+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。注意:以下代碼被縮略過,只保留核心代碼,框架版本:4.3.0
首先先從執(zhí)行用例時的命令開始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)
},
// 【重點】:執(zhí)行命令后調(diào)用函數(shù)
RunE: func(cmd *cobra.Command, args []string) error {
// 【疑問】:為什么存放用例路徑的數(shù)組類型是:hrp.ITestCase
var paths []hrp.ITestCase
// 編輯命令參數(shù)獲取所有待執(zhí)行用例的路徑
for _, arg := range args {
path := hrp.TestCasePath(arg)
paths = append(paths, &path)
}
// 創(chuàng)建運行器
runner := makeHRPRunner()
// 調(diào)用執(zhí)行函數(shù)
return runner.Run(paths...)
},
}
總結(jié)
在執(zhí)行命令后,代碼處理了創(chuàng)建運行器,根據(jù)命令行參數(shù)初始化一個運行器
調(diào)用執(zhí)行函數(shù),將待執(zhí)行用例作為參數(shù)傳入
其實hrp.ITestCase是一個接口,由于框架本身要支持多種用例類型:go test/文件類型/curl... 需要將不同類型的用例轉(zhuǎn)換成一個相同結(jié)構(gòu)在運行,ITestCase 接口就是定義一個規(guī)范來實現(xiàn)統(tǒng)一結(jié)構(gòu)。
// 不同的用例格式,只需要實現(xiàn)ITestCase接口定義的兩個方法即可通過運行器運行
type ITestCase interface {
GetPath() string
ToTestCase() (*TestCase, error)
}
而在接收到命令行參數(shù)后有將參數(shù)轉(zhuǎn)換:hrp.TestCasePath(arg)
TestCasePath 是 string 類型的別名,同時實現(xiàn)了ITestCase接口,所以用例路徑可以轉(zhuǎn)為: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)
}
執(zhí)行命令后調(diào)用Run方法進行處理func (r *HRPRunner) Run(testcases ...ITestCase) error {
// ··· 縮略代碼
// 初始化執(zhí)行摘要,用于存儲執(zhí)行結(jié)果
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 {
// 【重點】每一條用例創(chuàng)建一個獨立的運行器
caseRunner, err := r.NewCaseRunner(testcase)
if err != nil {
log.Error().Err(err).Msg("[Run] init case runner failed")
return err
}
// ... 縮略代碼
// 【重點】迭代器,負(fù)責(zé)參數(shù)化迭代
// 【疑問】當(dāng)用例沒有參數(shù)化時,迭代器會運行嗎?
for it := caseRunner.parametersIterator; it.HasNext(); {
// case runner can run multiple times with different parameters
// each run has its own session runner
// 【重點】為每一次參數(shù)迭代創(chuàng)建一個會話運行器
sessionRunner := caseRunner.NewSession()
// 【重點】啟動會話運行器
err1 := sessionRunner.Start(it.Next())
if err1 != nil {
log.Error().Err(err1).Msg("[Run] run testcase failed")
runErr = err1
}
// 【重點】獲取會話的運行結(jié)果,
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
}
}
// 運行錯誤時跳出當(dāng)前迭代
if runErr != nil && r.failfast {
break
}
}
}
// 獲取運行時長
s.Time.Duration = time.Since(s.Time.StartAt).Seconds()
// 【重點】保存測試結(jié)果
if r.saveTests {
err := s.genSummary()
if err != nil {
return err
}
}
// 【重點】生成測試報告
if r.genHTMLReport {
err := s.genHTMLReport()
if err != nil {
return err
}
}
return runErr
}
總結(jié)
Run方法為實際執(zhí)行用例的入口答案肯定是會運行,我們在實際使用中肯定遇到過不需要參數(shù)化的場景,那在沒有參數(shù)化時迭代器是怎樣執(zhí)行的呢?
通過分析下面這塊代碼,發(fā)現(xiàn)其實想要執(zhí)行用例,只需要滿足it.HasNext()即可
// 滿足:it.HasNext() 即可進入循環(huán)
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
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
}
想知道上述條件是怎么設(shè)置的還要從初始化ParametersIterator開始,通過下面代碼分析,在沒有設(shè)置參數(shù)化時初始化代碼剛好滿足條件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,
}
// 【重點】當(dāng)parameters的長度等于0的時候 limit = 1
if len(parameters) == 0 {
iterator.data = map[string]Parameters{}
iterator.limit = 1
return iterator
}
// ... 省略代碼
return iterator
}
此時滿足了it.HasNext(),代碼繼續(xù)執(zhí)行會發(fā)現(xiàn)在Start()函數(shù)執(zhí)行的時候,還傳入了it.Next()
既然都沒有參數(shù)化,那it.Next()會發(fā)生什么事呢?
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 此時不進入循環(huán)
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 ++ 后 保證下次調(diào)用it.HasNext() == false
iter.index++
if iter.limit >0 && iter.index >= iter.limit {
iter.hasNext = false
}
// 【重點】最終會返回一個空map
return selectedParameters
}
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧