0


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

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
}

本文转载自: https://blog.csdn.net/weixin_45346122/article/details/128160281
版权归原作者 不休不止~ 所有, 如有侵权,请联系我们删除。

“HttpRunner v4 一条用例是怎么被执行的”的评论:

还没有评论