mirror of
https://github.com/httprunner/httprunner.git
synced 2026-06-25 17:44:02 +08:00
refactor: TestCase/IConfig models
This commit is contained in:
@@ -6,9 +6,12 @@ HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤
|
||||
|
||||
```go
|
||||
type TestCase struct {
|
||||
Config *TConfig `json:"config" yaml:"config"`
|
||||
Steps []*TStep `json:"teststeps" yaml:"teststeps"`
|
||||
TestSteps []IStep `json:"-" yaml:"-"`
|
||||
Config IConfig `json:"config" yaml:"config"`
|
||||
TestSteps []IStep `json:"teststeps" yaml:"teststeps"`
|
||||
}
|
||||
|
||||
type IConfig interface {
|
||||
Get() *TConfig
|
||||
}
|
||||
```
|
||||
|
||||
@@ -18,7 +21,7 @@ type TestCase struct {
|
||||
type IStep interface {
|
||||
Name() string
|
||||
Type() StepType
|
||||
Struct() *TStep
|
||||
Config() *StepConfig
|
||||
Run(*SessionRunner) (*StepResult, error)
|
||||
}
|
||||
```
|
||||
@@ -32,6 +35,7 @@ type IStep interface {
|
||||
- [transaction](step_transaction.go):事务机制,用于压测
|
||||
- [rendezvous](step_rendezvous.go):集合点机制,用于压测
|
||||
- [mobile_UI](step_mobile_ui.go):移动端 UI 自动化
|
||||
- [shell](step_shell.go):执行 shell 命令
|
||||
|
||||
基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。
|
||||
|
||||
@@ -53,7 +57,7 @@ type HRPRunner struct {
|
||||
}
|
||||
|
||||
func (r *HRPRunner) Run(testcases ...ITestCase) error
|
||||
func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error)
|
||||
```
|
||||
|
||||
重点关注两个方法:
|
||||
@@ -75,7 +79,7 @@ type CaseRunner struct {
|
||||
parametersIterator *ParametersIterator
|
||||
}
|
||||
|
||||
func (r *CaseRunner) NewSession() *SessionRunner {
|
||||
func (r *CaseRunner) NewSession() *SessionRunner
|
||||
```
|
||||
|
||||
重点关注一个方法:
|
||||
|
||||
@@ -138,9 +138,10 @@ func (b *HRPBoomer) ParseTestCases(testCases []*TestCase) []*TestCase {
|
||||
log.Error().Err(err).Msg("failed to create runner")
|
||||
os.Exit(code.GetErrorCode(err))
|
||||
}
|
||||
caseRunner.Config.Parameters = caseRunner.parametersIterator.outParameters()
|
||||
caseConfig := caseRunner.TestCase.Config.Get()
|
||||
caseConfig.Parameters = caseRunner.parametersIterator.outParameters()
|
||||
parsedTestCases = append(parsedTestCases, &TestCase{
|
||||
Config: caseRunner.Config,
|
||||
Config: caseConfig,
|
||||
TestSteps: caseRunner.TestSteps,
|
||||
})
|
||||
}
|
||||
@@ -184,19 +185,20 @@ func (b *HRPBoomer) parseTCases(testCases []*TestCase) (testcases []ITestCase) {
|
||||
return
|
||||
}
|
||||
|
||||
if tc.Config.PluginSetting != nil {
|
||||
tc.Config.PluginSetting.Path = filepath.Join(tempDir, fmt.Sprintf("debugtalk.%s", tc.Config.PluginSetting.Type))
|
||||
err = builtin.Bytes2File(tc.Config.PluginSetting.Content, tc.Config.PluginSetting.Path)
|
||||
caseConfig := tc.Config.Get()
|
||||
if caseConfig.PluginSetting != nil {
|
||||
caseConfig.PluginSetting.Path = filepath.Join(tempDir, fmt.Sprintf("debugtalk.%s", caseConfig.PluginSetting.Type))
|
||||
err = builtin.Bytes2File(caseConfig.PluginSetting.Content, caseConfig.PluginSetting.Path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to save plugin file")
|
||||
return
|
||||
}
|
||||
tc.Config.PluginSetting.Content = nil // remove the content in testcase
|
||||
caseConfig.PluginSetting.Content = nil // remove the content in testcase
|
||||
}
|
||||
|
||||
if tc.Config.Environs != nil {
|
||||
if caseConfig.Environs != nil {
|
||||
envContent := ""
|
||||
for k, v := range tc.Config.Environs {
|
||||
for k, v := range caseConfig.Environs {
|
||||
envContent += fmt.Sprintf("%s=%s\n", k, v)
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(tempDir, ".env"), []byte(envContent), 0o644)
|
||||
@@ -206,8 +208,8 @@ func (b *HRPBoomer) parseTCases(testCases []*TestCase) (testcases []ITestCase) {
|
||||
}
|
||||
}
|
||||
|
||||
tc.Config.Path = filepath.Join(tempDir, "test-case.json")
|
||||
err = builtin.Dump2JSON(tc, tc.Config.Path)
|
||||
caseConfig.Path = filepath.Join(tempDir, "test-case.json")
|
||||
err = builtin.Dump2JSON(tc, caseConfig.Path)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to dump testcases")
|
||||
return
|
||||
@@ -335,8 +337,8 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
|
||||
mutex := sync.Mutex{}
|
||||
|
||||
return &boomer.Task{
|
||||
Name: testcase.Config.Name,
|
||||
Weight: testcase.Config.Weight,
|
||||
Name: testcase.Config.Get().Name,
|
||||
Weight: testcase.Config.Get().Weight,
|
||||
Fn: func() {
|
||||
testcaseSuccess := true // flag whole testcase result
|
||||
transactionSuccess := true // flag current transaction result
|
||||
|
||||
@@ -7,6 +7,10 @@ import (
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
|
||||
)
|
||||
|
||||
type IConfig interface {
|
||||
Get() *TConfig
|
||||
}
|
||||
|
||||
// NewConfig returns a new constructed testcase config with specified testcase name.
|
||||
func NewConfig(name string) *TConfig {
|
||||
return &TConfig{
|
||||
@@ -39,6 +43,10 @@ type TConfig struct {
|
||||
PluginSetting *PluginConfig `json:"plugin,omitempty" yaml:"plugin,omitempty"` // plugin config
|
||||
}
|
||||
|
||||
func (c *TConfig) Get() *TConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
// WithVariables sets variables for current testcase.
|
||||
func (c *TConfig) WithVariables(variables map[string]interface{}) *TConfig {
|
||||
c.Variables = variables
|
||||
|
||||
@@ -1 +1 @@
|
||||
v5.0.0+2411092015
|
||||
v5.0.0+2411092105
|
||||
|
||||
@@ -282,9 +282,10 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
hrpRunner: r,
|
||||
parser: newParser(),
|
||||
}
|
||||
config := testcase.Config.Get()
|
||||
|
||||
// init parser plugin
|
||||
plugin, err := initPlugin(testcase.Config.Path, r.venv, r.pluginLogOn)
|
||||
plugin, err := initPlugin(config.Path, r.venv, r.pluginLogOn)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "init plugin failed")
|
||||
}
|
||||
@@ -297,27 +298,26 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parse testcase config failed")
|
||||
}
|
||||
caseRunner.TestCase.Config = parsedConfig
|
||||
|
||||
// set request timeout in seconds
|
||||
if testcase.Config.RequestTimeout != 0 {
|
||||
r.SetRequestTimeout(testcase.Config.RequestTimeout)
|
||||
if config.RequestTimeout != 0 {
|
||||
r.SetRequestTimeout(config.RequestTimeout)
|
||||
}
|
||||
// set testcase timeout in seconds
|
||||
if testcase.Config.CaseTimeout != 0 {
|
||||
r.SetCaseTimeout(testcase.Config.CaseTimeout)
|
||||
if config.CaseTimeout != 0 {
|
||||
r.SetCaseTimeout(config.CaseTimeout)
|
||||
}
|
||||
|
||||
// load plugin info to testcase config
|
||||
if plugin != nil {
|
||||
pluginPath, _ := locatePlugin(testcase.Config.Path)
|
||||
if caseRunner.Config.PluginSetting == nil {
|
||||
pluginPath, _ := locatePlugin(config.Path)
|
||||
if parsedConfig.PluginSetting == nil {
|
||||
pluginContent, err := readFile(pluginPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tp := strings.Split(plugin.Path(), ".")
|
||||
caseRunner.Config.PluginSetting = &PluginConfig{
|
||||
parsedConfig.PluginSetting = &PluginConfig{
|
||||
Path: pluginPath,
|
||||
Content: pluginContent,
|
||||
Type: tp[len(tp)-1],
|
||||
@@ -325,6 +325,7 @@ func (r *HRPRunner) NewCaseRunner(testcase TestCase) (*CaseRunner, error) {
|
||||
}
|
||||
}
|
||||
|
||||
caseRunner.TestCase.Config = parsedConfig
|
||||
return caseRunner, nil
|
||||
}
|
||||
|
||||
@@ -339,7 +340,7 @@ type CaseRunner struct {
|
||||
|
||||
// parseConfig parses testcase config, stores to parsedConfig.
|
||||
func (r *CaseRunner) parseConfig() (parsedConfig *TConfig, err error) {
|
||||
cfg := r.TestCase.Config
|
||||
cfg := r.TestCase.Config.Get()
|
||||
|
||||
parsedConfig = &TConfig{}
|
||||
// deep copy config to avoid data racing
|
||||
@@ -541,7 +542,7 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa
|
||||
// report GA event
|
||||
sdk.SendGA4Event("hrp_session_runner_start", nil)
|
||||
|
||||
config := r.caseRunner.Config
|
||||
config := r.caseRunner.TestCase.Config.Get()
|
||||
log.Info().Str("testcase", config.Name).Msg("run testcase start")
|
||||
|
||||
// update config variables with given variables
|
||||
@@ -552,14 +553,14 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa
|
||||
r.releaseResources()
|
||||
|
||||
summary = r.summary
|
||||
summary.Name = r.caseRunner.Config.Name
|
||||
summary.Name = config.Name
|
||||
summary.Time.Duration = time.Since(summary.Time.StartAt).Seconds()
|
||||
exportVars := make(map[string]interface{})
|
||||
for _, value := range r.caseRunner.Config.Export {
|
||||
for _, value := range config.Export {
|
||||
exportVars[value] = r.sessionVariables[value]
|
||||
}
|
||||
summary.InOut.ExportVars = exportVars
|
||||
summary.InOut.ConfigVars = r.caseRunner.Config.Variables
|
||||
summary.InOut.ConfigVars = config.Variables
|
||||
|
||||
// TODO: move to mobile ui step
|
||||
for uuid, client := range uiClients {
|
||||
@@ -682,18 +683,19 @@ func (r *SessionRunner) Start(givenVars map[string]interface{}) (summary *TestCa
|
||||
}
|
||||
|
||||
func (r *SessionRunner) parseStep(step IStep) error {
|
||||
caseConfig := r.caseRunner.TestCase.Config.Get()
|
||||
stepConfig := step.Config()
|
||||
|
||||
// update step variables: merges step variables with config variables and session variables
|
||||
// variables priority: step variables > session variables (extracted variables from previous steps)
|
||||
overrideVars := mergeVariables(stepConfig.Variables, r.sessionVariables)
|
||||
// step variables > testcase config variables
|
||||
overrideVars = mergeVariables(overrideVars, r.caseRunner.Config.Variables)
|
||||
overrideVars = mergeVariables(overrideVars, caseConfig.Variables)
|
||||
|
||||
// parse step variables
|
||||
parsedVariables, err := r.caseRunner.parser.ParseVariables(overrideVars)
|
||||
if err != nil {
|
||||
log.Error().Interface("variables", r.caseRunner.Config.Variables).
|
||||
log.Error().Interface("variables", caseConfig.Variables).
|
||||
Err(err).Msg("parse step variables failed")
|
||||
return errors.Wrap(err, "parse step variables failed")
|
||||
}
|
||||
@@ -750,11 +752,12 @@ func (r *SessionRunner) initWithParameters(parameters map[string]interface{}) {
|
||||
}
|
||||
|
||||
func (r *SessionRunner) IgnorePopup() bool {
|
||||
if r.caseRunner.TestCase.Config.Android != nil {
|
||||
return r.caseRunner.TestCase.Config.Android[0].IgnorePopup
|
||||
caseConfig := r.caseRunner.TestCase.Config.Get()
|
||||
if caseConfig.Android != nil {
|
||||
return caseConfig.Android[0].IgnorePopup
|
||||
}
|
||||
if r.caseRunner.TestCase.Config.IOS != nil {
|
||||
return r.caseRunner.TestCase.Config.IOS[0].IgnorePopup
|
||||
if caseConfig.IOS != nil {
|
||||
return caseConfig.IOS[0].IgnorePopup
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ func runStepRequest(r *SessionRunner, step IStep) (stepResult *StepResult, err e
|
||||
|
||||
sessionData := newSessionData()
|
||||
parser := r.caseRunner.parser
|
||||
config := r.caseRunner.Config
|
||||
config := r.caseRunner.Config.Get()
|
||||
|
||||
rb := newRequestBuilder(parser, config, stepRequest.Request)
|
||||
rb.req.Method = strings.ToUpper(string(stepRequest.Request.Method))
|
||||
|
||||
@@ -103,7 +103,7 @@ func runStepShell(r *SessionRunner, step IStep) (stepResult *StepResult, err err
|
||||
ContentSize: 0,
|
||||
}
|
||||
|
||||
vars := r.caseRunner.Config.Variables
|
||||
vars := r.caseRunner.Config.Get().Variables
|
||||
for key, value := range vars {
|
||||
os.Setenv(key, fmt.Sprintf("%v", value))
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (s *StepTestCaseWithOptionalArgs) Name() string {
|
||||
}
|
||||
ts, ok := s.TestCase.(*TestCase)
|
||||
if ok {
|
||||
return ts.Config.Name
|
||||
return ts.Config.Get().Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -68,13 +68,14 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (stepResult *StepRe
|
||||
return stepResult, err
|
||||
}
|
||||
|
||||
config := copiedTestCase.Config.Get()
|
||||
// override testcase config
|
||||
// override testcase name
|
||||
if s.StepName != "" {
|
||||
copiedTestCase.Config.Name = s.StepName
|
||||
config.Name = s.StepName
|
||||
}
|
||||
// merge & override extractors
|
||||
copiedTestCase.Config.Export = mergeSlices(s.StepExport, copiedTestCase.Config.Export)
|
||||
config.Export = mergeSlices(s.StepExport, config.Export)
|
||||
|
||||
caseRunner, err := r.caseRunner.hrpRunner.NewCaseRunner(*copiedTestCase)
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
|
||||
Success: true,
|
||||
}
|
||||
|
||||
cfg := r.caseRunner.Config.ThinkTimeSetting
|
||||
cfg := r.caseRunner.Config.Get().ThinkTimeSetting
|
||||
if cfg == nil {
|
||||
cfg = &ThinkTimeConfig{thinkTimeDefault, nil, 0}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ func runStepWebSocket(r *SessionRunner, step IStep) (stepResult *StepResult, err
|
||||
|
||||
sessionData := newSessionData()
|
||||
parser := r.caseRunner.parser
|
||||
config := r.caseRunner.Config
|
||||
config := r.caseRunner.Config.Get()
|
||||
|
||||
dummyReq := &Request{
|
||||
URL: webSocket.URL,
|
||||
@@ -706,7 +706,7 @@ func (r *SessionRunner) releaseResources() {
|
||||
// close websocket connections
|
||||
for _, wsConn := range r.ws.wsConnMap {
|
||||
if wsConn != nil {
|
||||
log.Info().Str("testcase", r.caseRunner.Config.Name).Msg("websocket disconnected")
|
||||
log.Info().Str("testcase", r.caseRunner.Config.Get().Name).Msg("websocket disconnected")
|
||||
err := wsConn.Close()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("websocket disconnection failed")
|
||||
|
||||
@@ -46,8 +46,8 @@ func (path *TestCasePath) GetTestCase() (*TestCase, error) {
|
||||
// TestCase is a container for one testcase, which is used for testcase runner.
|
||||
// TestCase implements ITestCase interface.
|
||||
type TestCase struct {
|
||||
Config *TConfig `json:"config" yaml:"config"`
|
||||
TestSteps []IStep `json:"teststeps" yaml:"teststeps"`
|
||||
Config IConfig `json:"config" yaml:"config"`
|
||||
TestSteps []IStep `json:"teststeps" yaml:"teststeps"`
|
||||
}
|
||||
|
||||
func (tc *TestCase) GetTestCase() (*TestCase, error) {
|
||||
@@ -126,13 +126,14 @@ func (tc *TestCaseDef) loadISteps() (*TestCase, error) {
|
||||
return nil, errors.Wrap(err, "failed to load .env file")
|
||||
}
|
||||
|
||||
config := testCase.Config.Get()
|
||||
// override testcase config env with variables loaded from .env file
|
||||
// priority: .env file > testcase config env
|
||||
if testCase.Config.Environs == nil {
|
||||
testCase.Config.Environs = make(map[string]string)
|
||||
if config.Environs == nil {
|
||||
config.Environs = make(map[string]string)
|
||||
}
|
||||
for key, value := range envVars {
|
||||
testCase.Config.Environs[key] = value
|
||||
config.Environs[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user