refactor: TestCase/IConfig models

This commit is contained in:
lilong.129
2024-11-09 21:05:30 +08:00
parent 8846e84d19
commit 3085a8492a
11 changed files with 71 additions and 52 deletions

View File

@@ -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
```
重点关注一个方法:

View File

@@ -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

View File

@@ -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

View File

@@ -1 +1 @@
v5.0.0+2411092015
v5.0.0+2411092105

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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}
}

View File

@@ -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")

View File

@@ -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
}
}