Merge pull request #1222 from httprunner/refactor-python

refactor: make step extensible to support implementing new protocols and test types for python version
This commit is contained in:
debugtalk
2022-04-03 00:27:08 +08:00
committed by GitHub
37 changed files with 1117 additions and 957 deletions
+1 -1
View File
@@ -3,6 +3,7 @@
## v4.0.0-alpha ## v4.0.0-alpha
- refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine - refactor: merge [hrp] into httprunner v4, which will include golang and python dual engine
- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types
**go version** **go version**
@@ -11,7 +12,6 @@
- change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis - change: integrate [sentry sdk][sentry sdk] for panic reporting and analysis
- change: lock funplugin version when creating scaffold project - change: lock funplugin version when creating scaffold project
- fix: call referenced api/testcase with relative path - fix: call referenced api/testcase with relative path
- refactor: redesign `IStep` to make step extensible to support implementing new protocols and test types
**python version** **python version**
+1 -1
View File
@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: basic.yml # FROM: basic.yml
+1 -1
View File
@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: hooks.yml # FROM: hooks.yml
+1 -1
View File
@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: load_image.yml # FROM: load_image.yml
+1 -1
View File
@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: upload.yml # FROM: upload.yml
+1 -1
View File
@@ -1,4 +1,4 @@
# NOTE: Generated By HttpRunner v3.1.7 # NOTE: Generated By HttpRunner v4.0.0-alpha
# FROM: validate.yml # FROM: validate.yml
+10 -8
View File
@@ -12,13 +12,13 @@ from httprunner.utils import get_platform, ExtendJSONEncoder
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def session_fixture(request): def session_fixture(request):
"""setup and teardown each task""" """setup and teardown each task"""
logger.info(f"start running testcases ...") logger.info("start running testcases ...")
start_at = time.time() start_at = time.time()
yield yield
logger.info(f"task finished, generate task summary for --save-tests") logger.info("task finished, generate task summary for --save-tests")
summary = { summary = {
"success": True, "success": True,
@@ -36,24 +36,27 @@ def session_fixture(request):
summary["success"] &= testcase_summary.success summary["success"] &= testcase_summary.success
summary["stat"]["testcases"]["total"] += 1 summary["stat"]["testcases"]["total"] += 1
summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_results)
if testcase_summary.success: if testcase_summary.success:
summary["stat"]["testcases"]["success"] += 1 summary["stat"]["testcases"]["success"] += 1
summary["stat"]["teststeps"]["successes"] += len( summary["stat"]["teststeps"]["successes"] += len(
testcase_summary.step_datas testcase_summary.step_results
) )
else: else:
summary["stat"]["testcases"]["fail"] += 1 summary["stat"]["testcases"]["fail"] += 1
summary["stat"]["teststeps"]["successes"] += ( summary["stat"]["teststeps"]["successes"] += (
len(testcase_summary.step_datas) - 1 len(testcase_summary.step_results) - 1
) )
summary["stat"]["teststeps"]["failures"] += 1 summary["stat"]["teststeps"]["failures"] += 1
testcase_summary_json = testcase_summary.dict() testcase_summary_json = testcase_summary.dict()
testcase_summary_json["records"] = testcase_summary_json.pop("step_datas") testcase_summary_json["records"] = testcase_summary_json.pop("step_results")
summary["details"].append(testcase_summary_json) summary["details"].append(testcase_summary_json)
summary_path = r"/Users/debugtalk/MyProjects/HttpRunner-dev/httprunner/examples/postman_echo/logs/request_methods/hardcode.summary.json" summary_path = os.path.join(
os.getcwd(),
"examples/postman_echo/logs/request_methods/hardcode.summary.json"
)
summary_dir = os.path.dirname(summary_path) summary_dir = os.path.dirname(summary_path)
os.makedirs(summary_dir, exist_ok=True) os.makedirs(summary_dir, exist_ok=True)
@@ -61,4 +64,3 @@ def session_fixture(request):
json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder) json.dump(summary, f, indent=4, ensure_ascii=False, cls=ExtendJSONEncoder)
logger.info(f"generated task summary: {summary_path}") logger.info(f"generated task summary: {summary_path}")
@@ -23,7 +23,7 @@ def session_fixture(request):
yield yield
logger.debug(f"teardown task fixture") logger.debug("teardown task fixture")
# teardown task # teardown task
# TODO: upload task summary # TODO: upload task summary
@@ -20,7 +20,6 @@ class TestCaseRequestWithFunctions(HttpRunner):
.base_url("https://postman-echo.com") .base_url("https://postman-echo.com")
.verify(False) .verify(False)
.export(*["foo3"]) .export(*["foo3"])
.locust_weight(2)
) )
teststeps = [ teststeps = [
@@ -29,7 +29,6 @@ class TestCaseRequestWithTestcaseReference(HttpRunner):
) )
.base_url("https://postman-echo.com") .base_url("https://postman-echo.com")
.verify(False) .verify(False)
.locust_weight(3)
) )
teststeps = [ teststeps = [
@@ -20,7 +20,6 @@ class TestCaseRequestWithFunctions(HttpRunner):
.base_url("https://postman-echo.com") .base_url("https://postman-echo.com")
.verify(False) .verify(False)
.export(*["foo3"]) .export(*["foo3"])
.locust_weight(2)
) )
teststeps = [ teststeps = [
+8 -7
View File
@@ -1,3 +1,4 @@
# 代码阅读指南(golang 部分)
## 核心数据结构 ## 核心数据结构
@@ -21,14 +22,14 @@ type IStep interface {
} }
``` ```
我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前已支持的步骤类型包括: 我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 hrp 已支持的步骤类型包括:
- request:发起单次 HTTP 请求 - [request](step_request.go):发起单次 HTTP 请求
- api:引用执行其它 API 文件 - [api](step_api.go):引用执行其它 API 文件
- testcase:引用执行其它测试用例文件 - [testcase](step_testcase.go):引用执行其它测试用例文件
- thinktime:思考时间,按照配置的逻辑进行等待 - [thinktime](step_thinktime.go):思考时间,按照配置的逻辑进行等待
- transaction:事务机制,用于压测 - [transaction](step_transaction.go):事务机制,用于压测
- rendezvous:集合点机制,用于压测 - [rendezvous](step_rendezvous.go):集合点机制,用于压测
基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。 基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。
+7 -7
View File
@@ -173,13 +173,13 @@ func (v *responseObject) Validate(iValidators []interface{}, variablesMapping ma
Msgf("validate %s", checkItem) Msgf("validate %s", checkItem)
if !result { if !result {
v.t.Fail() v.t.Fail()
return errors.New(fmt.Sprintf( log.Error().
"do assertion failed, checkExpr: %v, assertMethod: %v, checkValue: %v, expectValue: %v", Str("checkExpr", validator.Check).
validator.Check, Str("assertMethod", assertMethod).
assertMethod, Interface("checkValue", checkValue).
checkValue, Interface("expectValue", expectValue).
expectValue, Msg("assert failed")
)) return errors.New("step validation failed")
} }
} }
return nil return nil
+25 -9
View File
@@ -69,24 +69,40 @@ func (r *SessionRunner) Start() error {
r.startTime = time.Now() r.startTime = time.Now()
// run step in sequential order // run step in sequential order
for _, step := range r.testCase.TestSteps { for _, step := range r.testCase.TestSteps {
_, err := step.Run(r) log.Info().Str("step", step.Name()).
Str("type", string(step.Type())).Msg("run step start")
stepResult, err := step.Run(r)
if err != nil && r.hrpRunner.failfast { if err != nil && r.hrpRunner.failfast {
log.Error().
Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", false).
Msg("run step end")
return errors.Wrap(err, "abort running due to failfast setting") return errors.Wrap(err, "abort running due to failfast setting")
} }
// update extracted variables
for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v
}
// update testcase summary
r.updateSummary(stepResult)
log.Info().
Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", stepResult.Success).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
} }
log.Info().Str("testcase", config.Name).Msg("run testcase end") log.Info().Str("testcase", config.Name).Msg("run testcase end")
return nil return nil
} }
func (r *SessionRunner) UpdateSession(vars map[string]interface{}) { // updateSummary appends step result to summary
for k, v := range vars { func (r *SessionRunner) updateSummary(stepResult *StepResult) {
r.sessionVariables[k] = v
}
}
// UpdateSummary appends step result to summary
func (r *SessionRunner) UpdateSummary(stepResult *StepResult) {
r.summary.Records = append(r.summary.Records, stepResult) r.summary.Records = append(r.summary.Records, stepResult)
r.summary.Stat.Total += 1 r.summary.Stat.Total += 1
if stepResult.Success { if stepResult.Success {
-4
View File
@@ -3,8 +3,6 @@ package hrp
import ( import (
"fmt" "fmt"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/hrp/internal/builtin" "github.com/httprunner/httprunner/hrp/internal/builtin"
) )
@@ -93,8 +91,6 @@ func (s *StepAPIWithOptionalArgs) Struct() *TStep {
} }
func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) { func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
log.Info().Str("api", s.step.Name).Msg("run referenced api")
// extend request with referenced API // extend request with referenced API
api, _ := s.step.API.(*API) api, _ := s.step.API.(*API)
extendWithAPI(s.step, api) extendWithAPI(s.step, api)
+6 -18
View File
@@ -243,8 +243,6 @@ func (r *requestBuilder) prepareBody(stepVariables map[string]interface{}) error
} }
func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) { func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) {
log.Info().Str("step", step.Name).Msg("run step start")
stepResult = &StepResult{ stepResult = &StepResult{
Name: step.Name, Name: step.Name,
StepType: stepTypeRequest, StepType: stepTypeRequest,
@@ -255,20 +253,16 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
defer func() { defer func() {
// update testcase summary // update testcase summary
if err != nil { if err != nil {
log.Error().Err(err).Msg("run request step failed")
stepResult.Attachment = err.Error() stepResult.Attachment = err.Error()
} else {
// update extracted variables
r.UpdateSession(stepResult.ExportVars)
log.Info().
Str("step", step.Name).
Bool("success", stepResult.Success).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
} }
r.UpdateSummary(stepResult)
}() }()
// override step variables
stepVariables, err := r.MergeStepVariables(step.Variables)
if err != nil {
return
}
sessionData := newSessionData() sessionData := newSessionData()
parser := r.GetParser() parser := r.GetParser()
config := r.GetConfig() config := r.GetConfig()
@@ -276,12 +270,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
rb := newRequestBuilder(parser, config, step.Request) rb := newRequestBuilder(parser, config, step.Request)
rb.req.Method = string(step.Request.Method) rb.req.Method = string(step.Request.Method)
// override step variables
stepVariables, err := r.MergeStepVariables(step.Variables)
if err != nil {
return
}
err = rb.prepareUrlParams(stepVariables) err = rb.prepareUrlParams(stepVariables)
if err != nil { if err != nil {
return return
+2 -11
View File
@@ -50,7 +50,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
} }
s.step.Variables = stepVariables s.step.Variables = stepVariables
log.Info().Str("testcase", s.step.Name).Msg("run referenced testcase")
stepResult := &StepResult{ stepResult := &StepResult{
Name: s.step.Name, Name: s.step.Name,
StepType: stepTypeTestCase, StepType: stepTypeTestCase,
@@ -73,16 +72,14 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
err = sessionRunner.Start() err = sessionRunner.Start()
stepResult.Elapsed = time.Since(start).Milliseconds() stepResult.Elapsed = time.Since(start).Milliseconds()
if err != nil { if err != nil {
log.Error().Err(err).Msg("run referenced testcase step failed")
log.Info().Str("step", s.step.Name).Bool("success", false).Msg("run step end")
stepResult.Attachment = err.Error() stepResult.Attachment = err.Error()
r.summary.Success = false r.summary.Success = false
return stepResult, err return stepResult, err
} }
summary := sessionRunner.GetSummary() summary := sessionRunner.GetSummary()
stepResult.Data = summary stepResult.Data = summary.Records
// export testcase export variables // export testcase export variables
stepResult.ExportVars = sessionRunner.summary.InOut.ExportVars stepResult.ExportVars = summary.InOut.ExportVars
stepResult.Success = true stepResult.Success = true
// update extracted variables // update extracted variables
@@ -96,12 +93,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
r.summary.Stat.Successes += summary.Stat.Successes r.summary.Stat.Successes += summary.Stat.Successes
r.summary.Stat.Failures += summary.Stat.Failures r.summary.Stat.Failures += summary.Stat.Failures
log.Info().
Str("step", s.step.Name).
Bool("success", true).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
return stepResult, nil return stepResult, nil
} }
+1 -2
View File
@@ -30,8 +30,7 @@ func (s *StepThinkTime) Struct() *TStep {
func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) { func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
thinkTime := s.step.ThinkTime thinkTime := s.step.ThinkTime
log.Info().Str("name", s.step.Name). log.Info().Float64("time", thinkTime.Time).Msg("think time")
Float64("time", thinkTime.Time).Msg("think time")
stepResult := &StepResult{ stepResult := &StepResult{
Name: s.step.Name, Name: s.step.Name,
+115
View File
@@ -0,0 +1,115 @@
# 代码阅读指南(python 部分)
## 核心数据结构
HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤的集合。
```py
class TestCase(BaseModel):
config: TConfig
teststeps: List[TStep]
```
针对每种测试步骤,统一继承自 `IStep`,并要求必须至少实现如下 4 个方法;步骤内容统一在 `run` 方法中进行实现。
```py
class IStep(object):
def name(self) -> str:
raise NotImplementedError
def type(self) -> str:
raise NotImplementedError
def struct(self) -> TStep:
raise NotImplementedError
def run(self, runner) -> StepData:
# runner: HttpRunner
raise NotImplementedError
```
我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 python 版本已支持的步骤类型包括:
- [request](step_request.py):发起单次 HTTP 请求
- [testcase](step_testcase.py):引用执行其它测试用例文件
基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。
## 用例编写
## 运行主流程
### 整体控制器 pytest
不同于 golang 版本,python 版本的控制逻辑都基于 `pytest` 的用例发现和执行机制。
- 如果是运行 JSON/YAML 格式的用例,hrp 会将用例转换为 pytest 支持的用例格式
- 如果是要自行编写 pytest 测试用例,需要遵循 HttpRunner 的格式要求
### pytest 用例格式要求
所有测试用例要求都继承自 `HttpRunner`,然后
结构如下所示:
```py
class TestCaseRequestWithFunctions(HttpRunner):
config = (
Config("request methods testcase with functions")
)
teststeps = [
Step(
RunRequest("get with params")...
),
Step(
RunRequest("post raw text")...
),
Step(
RunRequest("post form data")...
),
]
```
完整案例可参考:
- [request_with_functions_test.py](../examples/postman_echo/request_methods/request_with_functions_test.py):用例中包含了 requests 的情况
- [request_with_testcase_reference_test.py](../examples/postman_echo/request_methods/request_with_testcase_reference_test.py):用例中包含了引用其它测试用例的情况
### 用例执行器 SessionRunner
测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。
```py
class SessionRunner(object):
config: Config
teststeps: List[object] # list of Step
...
```
重点关注一个方法:
- test_start:该方法将被 pytest 发现,作为启动执行入口,依次执行所有测试步骤
```go
def test_start(self, param: Dict = None) -> "SessionRunner":
"""main entrance, discovered by pytest"""
self.__start_at = time.time()
try:
# run step in sequential order
for step in self.teststeps:
self.__run_step(step)
finally:
logger.info(f"generate testcase log: {self.__log_path}")
self.__duration = time.time() - self.__start_at
```
在主流程中,SessionRunner 并不需要关注 step 的具体类型,统一都是调用 `step.run(self)`,具体实现逻辑都在对应 step 的 `run` 方法中。
```py
def run(self, runner: HttpRunner) -> StepData:
return self.__step.run(runner)
```
+4 -2
View File
@@ -1,10 +1,12 @@
__version__ = "4.0.0-alpha" __version__ = "4.0.0-alpha"
__description__ = "One-stop solution for HTTP(S) testing." __description__ = "One-stop solution for HTTP(S) testing."
# import firstly for monkey patch if needed from httprunner.config import Config
from httprunner.parser import parse_parameters as Parameters from httprunner.parser import parse_parameters as Parameters
from httprunner.runner import HttpRunner from httprunner.runner import HttpRunner
from httprunner.testcase import Config, Step, RunRequest, RunTestCase from httprunner.step import Step
from httprunner.step_request import RunRequest
from httprunner.step_testcase import RunTestCase
__all__ = [ __all__ = [
"__version__", "__version__",
+2 -2
View File
@@ -63,7 +63,7 @@ def main():
) )
subparsers = parser.add_subparsers(help="sub-command help") subparsers = parser.add_subparsers(help="sub-command help")
sub_parser_run = init_parser_run(subparsers) init_parser_run(subparsers)
sub_parser_make = init_make_parser(subparsers) sub_parser_make = init_make_parser(subparsers)
if len(sys.argv) == 1: if len(sys.argv) == 1:
@@ -93,7 +93,7 @@ def main():
sys.exit(0) sys.exit(0)
extra_args = [] extra_args = []
if len(sys.argv) >= 2 and sys.argv[1] in ["run", "locusts"]: if len(sys.argv) >= 2 and sys.argv[1] in ["run"]:
args, extra_args = parser.parse_known_args() args, extra_args = parser.parse_known_args()
else: else:
args = parser.parse_args() args = parser.parse_args()
+12 -2
View File
@@ -5,7 +5,8 @@ import unittest
import pytest import pytest
from httprunner.cli import main from httprunner import loader
from httprunner.cli import main, main_run
class TestCli(unittest.TestCase): class TestCli(unittest.TestCase):
@@ -45,8 +46,17 @@ class TestCli(unittest.TestCase):
try: try:
os.chdir(os.path.join(cwd, "examples", "postman_echo")) os.chdir(os.path.join(cwd, "examples", "postman_echo"))
exit_code = pytest.main( exit_code = pytest.main(
["-s", "request_methods/request_with_testcase_reference_test.py",] ["-s", "request_methods/request_with_testcase_reference_test.py"]
) )
self.assertEqual(exit_code, 0) self.assertEqual(exit_code, 0)
finally: finally:
os.chdir(cwd) os.chdir(cwd)
def test_run_testcase_with_abnormal_path(self):
loader.project_meta = None
exit_code = main_run(["examples/data/a-b.c/2 3.yml"])
self.assertEqual(exit_code, 0)
self.assertTrue(os.path.exists("examples/data/a_b_c/__init__.py"))
self.assertTrue(os.path.exists("examples/data/debugtalk.py"))
self.assertTrue(os.path.exists("examples/data/a_b_c/T1_test.py"))
self.assertTrue(os.path.exists("examples/data/a_b_c/T2_3_test.py"))
+7 -7
View File
@@ -257,12 +257,12 @@ def ensure_cli_args(args: List) -> List:
""" """
# remove deprecated --failfast # remove deprecated --failfast
if "--failfast" in args: if "--failfast" in args:
logger.warning(f"remove deprecated argument: --failfast") logger.warning("remove deprecated argument: --failfast")
args.pop(args.index("--failfast")) args.pop(args.index("--failfast"))
# convert --report-file to --html # convert --report-file to --html
if "--report-file" in args: if "--report-file" in args:
logger.warning(f"replace deprecated argument --report-file with --html") logger.warning("replace deprecated argument --report-file with --html")
index = args.index("--report-file") index = args.index("--report-file")
args[index] = "--html" args[index] = "--html"
args.append("--self-contained-html") args.append("--self-contained-html")
@@ -270,7 +270,7 @@ def ensure_cli_args(args: List) -> List:
# keep compatibility with --save-tests in v2 # keep compatibility with --save-tests in v2
if "--save-tests" in args: if "--save-tests" in args:
logger.warning( logger.warning(
f"generate conftest.py keep compatibility with --save-tests in v2" "generate conftest.py keep compatibility with --save-tests in v2"
) )
args.pop(args.index("--save-tests")) args.pop(args.index("--save-tests"))
_generate_conftest_for_summary(args) _generate_conftest_for_summary(args)
@@ -327,21 +327,21 @@ def session_fixture(request):
summary["success"] &= testcase_summary.success summary["success"] &= testcase_summary.success
summary["stat"]["testcases"]["total"] += 1 summary["stat"]["testcases"]["total"] += 1
summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_datas) summary["stat"]["teststeps"]["total"] += len(testcase_summary.step_results)
if testcase_summary.success: if testcase_summary.success:
summary["stat"]["testcases"]["success"] += 1 summary["stat"]["testcases"]["success"] += 1
summary["stat"]["teststeps"]["successes"] += len( summary["stat"]["teststeps"]["successes"] += len(
testcase_summary.step_datas testcase_summary.step_results
) )
else: else:
summary["stat"]["testcases"]["fail"] += 1 summary["stat"]["testcases"]["fail"] += 1
summary["stat"]["teststeps"]["successes"] += ( summary["stat"]["teststeps"]["successes"] += (
len(testcase_summary.step_datas) - 1 len(testcase_summary.step_results) - 1
) )
summary["stat"]["teststeps"]["failures"] += 1 summary["stat"]["teststeps"]["failures"] += 1
testcase_summary_json = testcase_summary.dict() testcase_summary_json = testcase_summary.dict()
testcase_summary_json["records"] = testcase_summary_json.pop("step_datas") testcase_summary_json["records"] = testcase_summary_json.pop("step_results")
summary["details"].append(testcase_summary_json) summary["details"].append(testcase_summary_json)
summary_path = r"{{SUMMARY_PATH_PLACEHOLDER}}" summary_path = r"{{SUMMARY_PATH_PLACEHOLDER}}"
+71
View File
@@ -0,0 +1,71 @@
import inspect
from typing import Text
from httprunner.models import TConfig, TConfigThrift
class ConfigThrift(object):
def __init__(self, config: TConfig) -> None:
self.__config = config
self.__config.thrift = TConfigThrift()
def psm(self, psm: Text) -> "ConfigThrift":
self.__config.thrift.psm = psm
return self
def env(self, env: Text) -> "ConfigThrift":
self.__config.thrift.env = env
return self
def cluster(self, cluster: Text) -> "ConfigThrift":
self.__config.thrift.cluster = cluster
return self
def target(self, target: Text) -> "ConfigThrift":
self.__config.thrift.target = target
return self
def struct(self) -> TConfig:
return self.__config
class Config(object):
def __init__(self, name: Text) -> None:
caller_frame = inspect.stack()[1]
self.__config = TConfig(
name=name,
path=caller_frame.filename
)
@property
def name(self) -> Text:
return self.__config.name
@property
def path(self) -> Text:
return self.__config.path
def variables(self, **variables) -> "Config":
self.__config.variables.update(variables)
return self
def base_url(self, base_url: Text) -> "Config":
self.__config.base_url = base_url
return self
def verify(self, verify: bool) -> "Config":
self.__config.verify = verify
return self
def export(self, *export_var_name: Text) -> "Config":
self.__config.export.extend(export_var_name)
self.__config.export = list(set(self.__config.export))
return self
def struct(self) -> TConfig:
return self.__config
def thrift(self) -> ConfigThrift:
return ConfigThrift(self.__config)
-7
View File
@@ -198,9 +198,6 @@ def make_config_chain_style(config: Dict) -> Text:
if "export" in config: if "export" in config:
config_chain_style += f'.export(*{config["export"]})' config_chain_style += f'.export(*{config["export"]})'
if "weight" in config:
config_chain_style += f'.locust_weight({config["weight"]})'
return config_chain_style return config_chain_style
@@ -484,10 +481,6 @@ def make_testsuite(testsuite: Dict):
) )
testcase_dict["config"]["variables"].update(testcase_variables) testcase_dict["config"]["variables"].update(testcase_variables)
# override weight
if "weight" in testcase:
testcase_dict["config"]["weight"] = testcase["weight"]
# make testcase # make testcase
testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir) testcase_pytest_path = make_testcase(testcase_dict, testsuite_dir)
pytest_files_run_set.add(testcase_pytest_path) pytest_files_run_set.add(testcase_pytest_path)
+40 -12
View File
@@ -1,11 +1,8 @@
import os import os
from enum import Enum from enum import Enum
from typing import Any from typing import Any, Callable, Dict, List, Text, Union
from typing import Dict, Text, Union, Callable
from typing import List
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, HttpUrl
from pydantic import HttpUrl
Name = Text Name = Text
Url = Text Url = Text
@@ -31,6 +28,16 @@ class MethodEnum(Text, Enum):
PATCH = "PATCH" PATCH = "PATCH"
# configs for thrift rpc
class TConfigThrift(BaseModel):
psm: Text = None
env: Text = None
cluster: Text = None
target: Text = None
include_dirs: List[Text] = None
thrift_client: Any = None
class TConfig(BaseModel): class TConfig(BaseModel):
name: Name name: Name
verify: Verify = False verify: Verify = False
@@ -42,7 +49,8 @@ class TConfig(BaseModel):
# teardown_hooks: Hooks = [] # teardown_hooks: Hooks = []
export: Export = [] export: Export = []
path: Text = None path: Text = None
weight: int = 1 # configs for other protocols
thrift: TConfigThrift = None
class TRequest(BaseModel): class TRequest(BaseModel):
@@ -153,16 +161,36 @@ class SessionData(BaseModel):
validators: Dict = {} validators: Dict = {}
class StepData(BaseModel): class StepResult(BaseModel):
"""teststep data, each step maybe corresponding to one request or one testcase""" """teststep data, each step maybe corresponding to one request or one testcase"""
name: Text = "" # teststep name
step_type: Text = "" # teststep type, request or testcase
success: bool = False success: bool = False
name: Text = "" # teststep name data: Union[SessionData, List['StepResult']] = None
data: Union[SessionData, List['StepData']] = None elapsed: float = 0.0 # teststep elapsed time
content_size: float = 0 # response content size
export_vars: VariablesMapping = {} export_vars: VariablesMapping = {}
attachment: Text = "" # teststep attachment
StepData.update_forward_refs() StepResult.update_forward_refs()
class IStep(object):
def name(self) -> str:
raise NotImplementedError
def type(self) -> str:
raise NotImplementedError
def struct(self) -> TStep:
raise NotImplementedError
def run(self, runner) -> StepResult:
# runner: HttpRunner
raise NotImplementedError
class TestCaseSummary(BaseModel): class TestCaseSummary(BaseModel):
@@ -172,7 +200,7 @@ class TestCaseSummary(BaseModel):
time: TestCaseTime time: TestCaseTime
in_out: TestCaseInOut = {} in_out: TestCaseInOut = {}
log: Text = "" log: Text = ""
step_datas: List[StepData] = [] step_results: List[StepResult] = []
class PlatformInfo(BaseModel): class PlatformInfo(BaseModel):
+22 -4
View File
@@ -1,14 +1,14 @@
import ast import ast
import builtins import builtins
import re
import os import os
from typing import Any, Set, Text, Callable, List, Dict, Union import re
from typing import Any, Callable, Dict, List, Set, Text
from loguru import logger from loguru import logger
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
from httprunner import loader, utils, exceptions from httprunner import exceptions, loader, utils
from httprunner.models import VariablesMapping, FunctionsMapping from httprunner.models import FunctionsMapping, VariablesMapping
absolute_http_url_regexp = re.compile(r"^https?://", re.I) absolute_http_url_regexp = re.compile(r"^https?://", re.I)
@@ -572,3 +572,21 @@ def parse_parameters(parameters: Dict,) -> List[Dict]:
parsed_parameters_list.append(parameter_content_list) parsed_parameters_list.append(parameter_content_list)
return utils.gen_cartesian_product(*parsed_parameters_list) return utils.gen_cartesian_product(*parsed_parameters_list)
class Parser(object):
def __init__(self, functions_mapping: FunctionsMapping = None) -> None:
self.functions_mapping = functions_mapping
def parse_string(self, raw_string: Text, variables_mapping: VariablesMapping) -> Any:
return parse_string(raw_string, variables_mapping, self.functions_mapping)
def parse_variables(self, variables_mapping: VariablesMapping) -> VariablesMapping:
return parse_variables_mapping(variables_mapping, self.functions_mapping)
def parse_data(self, raw_data: Any, variables_mapping: VariablesMapping = None) -> Any:
return parse_data(raw_data, variables_mapping, self.functions_mapping)
def get_mapping_function(self, func_name: Text) -> Callable:
return get_mapping_function(func_name, self.functions_mapping)
+11 -13
View File
@@ -7,8 +7,8 @@ from loguru import logger
from httprunner import exceptions from httprunner import exceptions
from httprunner.exceptions import ValidationFailure, ParamsError from httprunner.exceptions import ValidationFailure, ParamsError
from httprunner.models import VariablesMapping, Validators, FunctionsMapping from httprunner.models import VariablesMapping, Validators
from httprunner.parser import parse_data, parse_string_value, get_mapping_function from httprunner.parser import parse_string_value, Parser
def get_uniform_comparator(comparator: Text): def get_uniform_comparator(comparator: Text):
@@ -115,7 +115,7 @@ def uniform_validator(validator):
class ResponseObject(object): class ResponseObject(object):
def __init__(self, resp_obj: requests.Response): def __init__(self, resp_obj: requests.Response, parser: Parser):
""" initialize with a requests.Response object """ initialize with a requests.Response object
Args: Args:
@@ -123,6 +123,7 @@ class ResponseObject(object):
""" """
self.resp_obj = resp_obj self.resp_obj = resp_obj
self.parser = parser
self.validation_results: Dict = {} self.validation_results: Dict = {}
def __getattr__(self, key): def __getattr__(self, key):
@@ -170,7 +171,6 @@ class ResponseObject(object):
def extract(self, def extract(self,
extractors: Dict[Text, Text], extractors: Dict[Text, Text],
variables_mapping: VariablesMapping = None, variables_mapping: VariablesMapping = None,
functions_mapping: FunctionsMapping = None,
) -> Dict[Text, Any]: ) -> Dict[Text, Any]:
if not extractors: if not extractors:
return {} return {}
@@ -179,8 +179,8 @@ class ResponseObject(object):
for key, field in extractors.items(): for key, field in extractors.items():
if '$' in field: if '$' in field:
# field contains variable or function # field contains variable or function
field = parse_data( field = self.parser.parse_data(
field, variables_mapping, functions_mapping field, variables_mapping
) )
field_value = self._search_jmespath(field) field_value = self._search_jmespath(field)
extract_mapping[key] = field_value extract_mapping[key] = field_value
@@ -192,11 +192,9 @@ class ResponseObject(object):
self, self,
validators: Validators, validators: Validators,
variables_mapping: VariablesMapping = None, variables_mapping: VariablesMapping = None,
functions_mapping: FunctionsMapping = None,
): ):
variables_mapping = variables_mapping or {} variables_mapping = variables_mapping or {}
functions_mapping = functions_mapping or {}
self.validation_results = {} self.validation_results = {}
if not validators: if not validators:
@@ -216,8 +214,8 @@ class ResponseObject(object):
check_item = u_validator["check"] check_item = u_validator["check"]
if "$" in check_item: if "$" in check_item:
# check_item is variable or function # check_item is variable or function
check_item = parse_data( check_item = self.parser.parse_data(
check_item, variables_mapping, functions_mapping check_item, variables_mapping
) )
check_item = parse_string_value(check_item) check_item = parse_string_value(check_item)
@@ -229,17 +227,17 @@ class ResponseObject(object):
# comparator # comparator
assert_method = u_validator["assert"] assert_method = u_validator["assert"]
assert_func = get_mapping_function(assert_method, functions_mapping) assert_func = self.parser.get_mapping_function(assert_method)
# expect item # expect item
expect_item = u_validator["expect"] expect_item = u_validator["expect"]
# parse expected value with config/teststep/extracted variables # parse expected value with config/teststep/extracted variables
expect_value = parse_data(expect_item, variables_mapping, functions_mapping) expect_value = self.parser.parse_data(expect_item, variables_mapping)
# message # message
message = u_validator["message"] message = u_validator["message"]
# parse message with config/teststep/extracted variables # parse message with config/teststep/extracted variables
message = parse_data(message, variables_mapping, functions_mapping) message = self.parser.parse_data(message, variables_mapping)
validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})" validate_msg = f"assert {check_item} {assert_method} {expect_value}({type(expect_value).__name__})"
+6 -7
View File
@@ -2,6 +2,7 @@ import unittest
import requests import requests
from httprunner.parser import Parser
from httprunner.response import ResponseObject from httprunner.response import ResponseObject
@@ -18,15 +19,16 @@ class TestResponse(unittest.TestCase):
] ]
}, },
) )
self.resp_obj = ResponseObject(resp) parser = Parser(functions_mapping={
'get_name': lambda: 'name',
"get_num": lambda x: x
})
self.resp_obj = ResponseObject(resp, parser)
def test_extract(self): def test_extract(self):
variables_mapping = { variables_mapping = {
'body': 'body' 'body': 'body'
} }
functions_mapping = {
'get_name': lambda: 'name',
}
extract_mapping = self.resp_obj.extract( extract_mapping = self.resp_obj.extract(
{ {
"var_1": "body.json.locations[0]", "var_1": "body.json.locations[0]",
@@ -35,7 +37,6 @@ class TestResponse(unittest.TestCase):
"var_4": "$body.json.locations[3].${get_name()}", "var_4": "$body.json.locations[3].${get_name()}",
}, },
variables_mapping=variables_mapping, variables_mapping=variables_mapping,
functions_mapping=functions_mapping,
) )
self.assertEqual(extract_mapping["var_1"], {"name": "Seattle", "state": "WA"}) self.assertEqual(extract_mapping["var_1"], {"name": "Seattle", "state": "WA"})
self.assertEqual(extract_mapping["var_2"], "Olympia") self.assertEqual(extract_mapping["var_2"], "Olympia")
@@ -62,9 +63,7 @@ class TestResponse(unittest.TestCase):
def test_validate_functions(self): def test_validate_functions(self):
variables_mapping = {"index": 1} variables_mapping = {"index": 1}
functions_mapping = {"get_num": lambda x: x}
self.resp_obj.validate( self.resp_obj.validate(
[{"eq": ["${get_num(0)}", 0]}, {"eq": ["${get_num($index)}", 1]},], [{"eq": ["${get_num(0)}", 0]}, {"eq": ["${get_num($index)}", 1]},],
variables_mapping=variables_mapping, variables_mapping=variables_mapping,
functions_mapping=functions_mapping,
) )
+119 -370
View File
@@ -2,7 +2,7 @@ import os
import time import time
import uuid import uuid
from datetime import datetime from datetime import datetime
from typing import List, Dict, Text from typing import Dict, List, Text
try: try:
import allure import allure
@@ -13,41 +13,29 @@ except ModuleNotFoundError:
from loguru import logger from loguru import logger
from httprunner import utils, exceptions
from httprunner.client import HttpSession from httprunner.client import HttpSession
from httprunner.exceptions import ValidationFailure, ParamsError from httprunner.config import Config
from httprunner.ext.uploader import prepare_upload_step from httprunner.exceptions import ParamsError
from httprunner.loader import load_project_meta, load_testcase_file from httprunner.loader import load_project_meta
from httprunner.parser import build_url, parse_data, parse_variables_mapping from httprunner.models import (ProjectMeta, StepResult, TConfig, TestCaseInOut,
from httprunner.response import ResponseObject TestCaseSummary, TestCaseTime, VariablesMapping)
from httprunner.testcase import Config, Step from httprunner.parser import Parser
from httprunner.utils import merge_variables from httprunner.utils import merge_variables
from httprunner.models import (
TConfig,
TStep,
VariablesMapping,
StepData,
TestCaseSummary,
TestCaseTime,
TestCaseInOut,
ProjectMeta,
TestCase,
Hooks,
)
class HttpRunner(object): class SessionRunner(object):
config: Config config: Config
teststeps: List[Step] teststeps: List[object] # list of Step
parser: Parser = None
session: HttpSession = None
case_id: Text = ""
root_dir: Text = ""
success: bool = False # indicate testcase execution result
__config: TConfig __config: TConfig
__teststeps: List[TStep]
__project_meta: ProjectMeta = None __project_meta: ProjectMeta = None
__case_id: Text = ""
__export: List[Text] = [] __export: List[Text] = []
__step_datas: List[StepData] = [] __step_results: List[StepResult] = []
__session: HttpSession = None
__session_variables: VariablesMapping = {} __session_variables: VariablesMapping = {}
# time # time
__start_at: float = 0 __start_at: float = 0
@@ -55,335 +43,62 @@ class HttpRunner(object):
# log # log
__log_path: Text = "" __log_path: Text = ""
def __init_tests__(self): def __init(self):
self.__config = self.config.perform() self.__config = self.config.struct()
self.__teststeps = [] self.__session_variables = {}
for step in self.teststeps: self.__start_at = 0
self.__teststeps.append(step.perform()) self.__duration = 0
@property
def raw_testcase(self) -> TestCase:
if not hasattr(self, "__config"):
self.__init_tests__()
return TestCase(config=self.__config, teststeps=self.__teststeps)
def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner":
self.__project_meta = project_meta
return self
def with_session(self, session: HttpSession) -> "HttpRunner":
self.__session = session
return self
def with_case_id(self, case_id: Text) -> "HttpRunner":
self.__case_id = case_id
return self
def with_variables(self, variables: VariablesMapping) -> "HttpRunner":
self.__session_variables = variables
return self
def with_export(self, export: List[Text]) -> "HttpRunner":
self.__export = export
return self
def __call_hooks(self, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text):
""" call hook actions.
Args:
hooks (list): each hook in hooks list maybe in two format.
format1 (str): only call hook functions.
${func()}
format2 (dict): assignment, the value returned by hook function will be assigned to variable.
{"var": "${func()}"}
step_variables: current step variables to call hook, include two special variables
request: parsed request dict
response: ResponseObject for current response
hook_msg: setup/teardown request/testcase
"""
logger.info(f"call hook actions: {hook_msg}")
if not isinstance(hooks, List):
logger.error(f"Invalid hooks format: {hooks}")
return
for hook in hooks:
if isinstance(hook, Text):
# format 1: ["${func()}"]
logger.debug(f"call hook function: {hook}")
parse_data(hook, step_variables, self.__project_meta.functions)
elif isinstance(hook, Dict) and len(hook) == 1:
# format 2: {"var": "${func()}"}
var_name, hook_content = list(hook.items())[0]
hook_content_eval = parse_data(
hook_content, step_variables, self.__project_meta.functions
)
logger.debug(
f"call hook function: {hook_content}, got value: {hook_content_eval}"
)
logger.debug(f"assign variable: {var_name} = {hook_content_eval}")
step_variables[var_name] = hook_content_eval
else:
logger.error(f"Invalid hook format: {hook}")
def __run_step_request(self, step: TStep) -> StepData:
"""run teststep: request"""
step_data = StepData(name=step.name)
# parse
functions = self.__project_meta.functions
prepare_upload_step(step, functions)
request_dict = step.request.dict()
request_dict.pop("upload", None)
parsed_request_dict = parse_data(
request_dict, step.variables, functions
)
parsed_request_dict["headers"].setdefault(
"HRUN-Request-ID",
f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
)
step.variables["request"] = parsed_request_dict
# setup hooks
if step.setup_hooks:
self.__call_hooks(step.setup_hooks, step.variables, "setup request")
# prepare arguments
method = parsed_request_dict.pop("method")
url_path = parsed_request_dict.pop("url")
url = build_url(self.__config.base_url, url_path)
parsed_request_dict["verify"] = self.__config.verify
parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
# request
resp = self.__session.request(method, url, **parsed_request_dict)
resp_obj = ResponseObject(resp)
step.variables["response"] = resp_obj
# teardown hooks
if step.teardown_hooks:
self.__call_hooks(step.teardown_hooks, step.variables, "teardown request")
def log_req_resp_details():
err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
# log request
err_msg += "====== request details ======\n"
err_msg += f"url: {url}\n"
err_msg += f"method: {method}\n"
headers = parsed_request_dict.pop("headers", {})
err_msg += f"headers: {headers}\n"
for k, v in parsed_request_dict.items():
v = utils.omit_long_data(v)
err_msg += f"{k}: {repr(v)}\n"
err_msg += "\n"
# log response
err_msg += "====== response details ======\n"
err_msg += f"status_code: {resp.status_code}\n"
err_msg += f"headers: {resp.headers}\n"
err_msg += f"body: {repr(resp.text)}\n"
logger.error(err_msg)
# extract
extractors = step.extract
extract_mapping = resp_obj.extract(extractors, step.variables, functions)
step_data.export_vars = extract_mapping
variables_mapping = step.variables
variables_mapping.update(extract_mapping)
# validate
validators = step.validators
session_success = False
try:
resp_obj.validate(
validators, variables_mapping, functions
)
session_success = True
except ValidationFailure:
session_success = False
log_req_resp_details()
# log testcase duration before raise ValidationFailure
self.__duration = time.time() - self.__start_at
raise
finally:
self.success = session_success
step_data.success = session_success
if hasattr(self.__session, "data"):
# httprunner.client.HttpSession, not locust.clients.HttpSession
# save request & response meta data
self.__session.data.success = session_success
self.__session.data.validators = resp_obj.validation_results
# save step data
step_data.data = self.__session.data
return step_data
def __run_step_testcase(self, step: TStep) -> StepData:
"""run teststep: referenced testcase"""
step_data = StepData(name=step.name)
step_variables = step.variables
step_export = step.export
# setup hooks
if step.setup_hooks:
self.__call_hooks(step.setup_hooks, step_variables, "setup testcase")
if hasattr(step.testcase, "config") and hasattr(step.testcase, "teststeps"):
testcase_cls = step.testcase
case_result = (
testcase_cls()
.with_session(self.__session)
.with_case_id(self.__case_id)
.with_variables(step_variables)
.with_export(step_export)
.run()
)
elif isinstance(step.testcase, Text):
if os.path.isabs(step.testcase):
ref_testcase_path = step.testcase
else:
ref_testcase_path = os.path.join(
self.__project_meta.RootDir, step.testcase
)
case_result = (
HttpRunner()
.with_session(self.__session)
.with_case_id(self.__case_id)
.with_variables(step_variables)
.with_export(step_export)
.run_path(ref_testcase_path)
)
else:
raise exceptions.ParamsError(
f"Invalid teststep referenced testcase: {step.dict()}"
)
# teardown hooks
if step.teardown_hooks:
self.__call_hooks(step.teardown_hooks, step.variables, "teardown testcase")
step_data.data = case_result.get_step_datas() # list of step data
step_data.export_vars = case_result.get_export_variables()
step_data.success = case_result.success
self.success = case_result.success
if step_data.export_vars:
logger.info(f"export variables: {step_data.export_vars}")
return step_data
def __run_step(self, step: TStep) -> Dict:
"""run teststep, teststep maybe a request or referenced testcase"""
logger.info(f"run step begin: {step.name} >>>>>>")
if step.request:
step_data = self.__run_step_request(step)
elif step.testcase:
step_data = self.__run_step_testcase(step)
else:
raise ParamsError(
f"teststep is neither a request nor a referenced testcase: {step.dict()}"
)
self.__step_datas.append(step_data)
logger.info(f"run step end: {step.name} <<<<<<\n")
return step_data.export_vars
def __parse_config(self, config: TConfig):
config.variables.update(self.__session_variables)
config.variables = parse_variables_mapping(
config.variables, self.__project_meta.functions
)
config.name = parse_data(
config.name, config.variables, self.__project_meta.functions
)
config.base_url = parse_data(
config.base_url, config.variables, self.__project_meta.functions
)
def run_testcase(self, testcase: TestCase) -> "HttpRunner":
"""run specified testcase
Examples:
>>> testcase_obj = TestCase(config=TConfig(...), teststeps=[TStep(...)])
>>> HttpRunner().with_project_meta(project_meta).run_testcase(testcase_obj)
"""
self.__config = testcase.config
self.__teststeps = testcase.teststeps
# prepare
self.__project_meta = self.__project_meta or load_project_meta( self.__project_meta = self.__project_meta or load_project_meta(
self.__config.path self.__config.path
) )
self.__parse_config(self.__config) self.case_id = self.case_id or str(uuid.uuid4())
self.__start_at = time.time() self.root_dir = self.root_dir or self.__project_meta.RootDir
self.__step_datas: List[StepData] = [] self.__log_path = os.path.join(
self.__session = self.__session or HttpSession() self.root_dir, "logs", f"{self.case_id}.run.log"
# save extracted variables of teststeps )
extracted_variables: VariablesMapping = {}
# run teststeps self.__step_results.clear()
for step in self.__teststeps: self.session = self.session or HttpSession()
# override variables self.parser = self.parser or Parser(self.__project_meta.functions)
# step variables > extracted variables from previous steps
step.variables = merge_variables(step.variables, extracted_variables)
# step variables > testcase config variables
step.variables = merge_variables(step.variables, self.__config.variables)
# parse variables def with_session(self, session: HttpSession) -> "SessionRunner":
step.variables = parse_variables_mapping( self.session = session
step.variables, self.__project_meta.functions
)
# run step
if USE_ALLURE:
with allure.step(f"step: {step.name}"):
extract_mapping = self.__run_step(step)
else:
extract_mapping = self.__run_step(step)
# save extracted variables to session variables
extracted_variables.update(extract_mapping)
self.__session_variables.update(extracted_variables)
self.__duration = time.time() - self.__start_at
return self return self
def run_path(self, path: Text) -> "HttpRunner": def get_config(self) -> TConfig:
if not os.path.isfile(path): return self.__config
raise exceptions.ParamsError(f"Invalid testcase path: {path}")
testcase_obj = load_testcase_file(path) def with_case_id(self, case_id: Text) -> "SessionRunner":
return self.run_testcase(testcase_obj) self.case_id = case_id
return self
def run(self) -> "HttpRunner": def with_variables(self, variables: VariablesMapping) -> "SessionRunner":
""" run current testcase self.__session_variables = variables
return self
Examples: def with_export(self, export: List[Text]) -> "SessionRunner":
>>> TestCaseRequestWithFunctions().run() self.__export = export
return self
""" def __parse_config(self, param: Dict = None) -> None:
self.__init_tests__() # parse config variables
testcase_obj = TestCase(config=self.__config, teststeps=self.__teststeps) self.__config.variables.update(self.__session_variables)
return self.run_testcase(testcase_obj) if param:
self.__config.variables.update(param)
self.__config.variables = self.parser.parse_variables(
self.__config.variables
)
def get_step_datas(self) -> List[StepData]: # parse config name
return self.__step_datas self.__config.name = self.parser.parse_data(
self.__config.name, self.__config.variables
)
# parse config base url
self.__config.base_url = self.parser.parse_data(
self.__config.base_url, self.__config.variables
)
def get_export_variables(self) -> Dict: def get_export_variables(self) -> Dict:
# override testcase export vars with step export # override testcase export vars with step export
@@ -403,10 +118,17 @@ class HttpRunner(object):
"""get testcase result summary""" """get testcase result summary"""
start_at_timestamp = self.__start_at start_at_timestamp = self.__start_at
start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat() start_at_iso_format = datetime.utcfromtimestamp(start_at_timestamp).isoformat()
summary_success = True
for step_result in self.__step_results:
if not step_result.success:
summary_success = False
break
return TestCaseSummary( return TestCaseSummary(
name=self.__config.name, name=self.__config.name,
success=self.success, success=summary_success,
case_id=self.__case_id, case_id=self.case_id,
time=TestCaseTime( time=TestCaseTime(
start_at=self.__start_at, start_at=self.__start_at,
start_at_iso_format=start_at_iso_format, start_at_iso_format=start_at_iso_format,
@@ -417,43 +139,70 @@ class HttpRunner(object):
export_vars=self.get_export_variables(), export_vars=self.get_export_variables(),
), ),
log=self.__log_path, log=self.__log_path,
step_datas=self.__step_datas, step_results=self.__step_results,
) )
def test_start(self, param: Dict = None) -> "HttpRunner": def merge_step_variables(self, variables: VariablesMapping) -> VariablesMapping:
# override variables
# step variables > extracted variables from previous steps
variables = merge_variables(variables, self.__session_variables)
# step variables > testcase config variables
variables = merge_variables(variables, self.__config.variables)
# parse variables
return self.parser.parse_variables(variables)
def __run_step(self, step):
"""run teststep, step maybe any kind that implements IStep interface
Args:
step (Step): teststep
"""
logger.info(f"run step begin: {step.name()} >>>>>>")
# run step
if USE_ALLURE:
with allure.step(f"step: {step.name()}"):
step_result: StepResult = step.run(self)
else:
step_result: StepResult = step.run(self)
# save extracted variables to session variables
self.__session_variables.update(step_result.export_vars)
# update testcase summary
self.__step_results.append(step_result)
logger.info(f"run step end: {step.name()} <<<<<<\n")
def test_start(self, param: Dict = None) -> "SessionRunner":
"""main entrance, discovered by pytest""" """main entrance, discovered by pytest"""
self.__init_tests__() self.__init()
self.__project_meta = self.__project_meta or load_project_meta( self.__parse_config(param)
self.__config.path
)
self.__case_id = self.__case_id or str(uuid.uuid4())
self.__log_path = self.__log_path or os.path.join(
self.__project_meta.RootDir, "logs", f"{self.__case_id}.run.log"
)
log_handler = logger.add(self.__log_path, level="DEBUG")
# parse config name
config_variables = self.__config.variables
if param:
config_variables.update(param)
config_variables.update(self.__session_variables)
self.__config.name = parse_data(
self.__config.name, config_variables, self.__project_meta.functions
)
if USE_ALLURE: if USE_ALLURE:
# update allure report meta # update allure report meta
allure.dynamic.title(self.__config.name) allure.dynamic.title(self.__config.name)
allure.dynamic.description(f"TestCase ID: {self.__case_id}") allure.dynamic.description(f"TestCase ID: {self.case_id}")
logger.info( logger.info(
f"Start to run testcase: {self.__config.name}, TestCase ID: {self.__case_id}" f"Start to run testcase: {self.__config.name}, TestCase ID: {self.case_id}"
) )
log_handler = logger.add(self.__log_path, level="DEBUG")
self.__start_at = time.time()
try: try:
return self.run_testcase( # run step in sequential order
TestCase(config=self.__config, teststeps=self.__teststeps) for step in self.teststeps:
) self.__run_step(step)
finally: finally:
logger.remove(log_handler) logger.remove(log_handler)
logger.info(f"generate testcase log: {self.__log_path}") logger.info(f"generate testcase log: {self.__log_path}")
self.__duration = time.time() - self.__start_at
return self
class HttpRunner(SessionRunner):
# split SessionRunner to keep consistant with golang version
pass
-40
View File
@@ -1,40 +0,0 @@
import os
import unittest
from httprunner import loader
from httprunner.cli import main_run
from httprunner.runner import HttpRunner
class TestHttpRunner(unittest.TestCase):
def setUp(self):
loader.project_meta = None
self.runner = HttpRunner()
def test_run_testcase_by_path_request_only(self):
self.runner.run_path(
"examples/postman_echo/request_methods/request_with_functions.yml"
)
result = self.runner.get_summary()
self.assertTrue(result.success)
self.assertEqual(result.name, "request methods testcase with functions")
self.assertEqual(result.step_datas[0].name, "get with params")
self.assertEqual(len(result.step_datas), 3)
def test_run_testcase_by_path_ref_testcase(self):
self.runner.run_path(
"examples/postman_echo/request_methods/request_with_testcase_reference.yml"
)
result = self.runner.get_summary()
self.assertTrue(result.success)
self.assertEqual(result.name, "request methods testcase: reference testcase")
self.assertEqual(result.step_datas[0].name, "request with functions")
self.assertEqual(len(result.step_datas), 2)
def test_run_testcase_with_abnormal_path(self):
exit_code = main_run(["examples/data/a-b.c/2 3.yml"])
self.assertEqual(exit_code, 0)
self.assertTrue(os.path.exists("examples/data/a_b_c/__init__.py"))
self.assertTrue(os.path.exists("examples/data/debugtalk.py"))
self.assertTrue(os.path.exists("examples/data/a_b_c/T1_test.py"))
self.assertTrue(os.path.exists("examples/data/a_b_c/T2_3_test.py"))
+40
View File
@@ -0,0 +1,40 @@
from typing import Union
from httprunner.models import StepResult, TRequest, TStep, TestCase
from httprunner.runner import HttpRunner
from httprunner.step_request import RequestWithOptionalArgs, StepRequestExtraction, StepRequestValidation
from httprunner.step_testcase import StepRefCase
class Step(object):
def __init__(
self,
step: Union[
StepRequestValidation,
StepRequestExtraction,
RequestWithOptionalArgs,
StepRefCase,
],
):
self.__step = step
@property
def request(self) -> TRequest:
return self.__step.struct().request
@property
def testcase(self) -> TestCase:
return self.__step.struct().testcase
def struct(self) -> TStep:
return self.__step.struct()
def name(self) -> str:
return self.__step.name()
def type(self) -> str:
return self.__step.type()
def run(self, runner: HttpRunner) -> StepResult:
return self.__step.run(runner)
+463
View File
@@ -0,0 +1,463 @@
import time
from typing import Any, Dict, List, Text, Union
from loguru import logger
from httprunner import utils
from httprunner.exceptions import ValidationFailure
from httprunner.ext.uploader import prepare_upload_step
from httprunner.models import (Hooks, IStep, MethodEnum, StepResult, TRequest,
TStep, VariablesMapping)
from httprunner.parser import build_url
from httprunner.response import ResponseObject
from httprunner.runner import HttpRunner
def call_hooks(runner: HttpRunner, hooks: Hooks, step_variables: VariablesMapping, hook_msg: Text):
""" call hook actions.
Args:
hooks (list): each hook in hooks list maybe in two format.
format1 (str): only call hook functions.
${func()}
format2 (dict): assignment, the value returned by hook function will be assigned to variable.
{"var": "${func()}"}
step_variables: current step variables to call hook, include two special variables
request: parsed request dict
response: ResponseObject for current response
hook_msg: setup/teardown request/testcase
"""
logger.info(f"call hook actions: {hook_msg}")
if not isinstance(hooks, List):
logger.error(f"Invalid hooks format: {hooks}")
return
for hook in hooks:
if isinstance(hook, Text):
# format 1: ["${func()}"]
logger.debug(f"call hook function: {hook}")
runner.parser.parse_data(hook, step_variables)
elif isinstance(hook, Dict) and len(hook) == 1:
# format 2: {"var": "${func()}"}
var_name, hook_content = list(hook.items())[0]
hook_content_eval = runner.parser.parse_data(
hook_content, step_variables
)
logger.debug(
f"call hook function: {hook_content}, got value: {hook_content_eval}"
)
logger.debug(f"assign variable: {var_name} = {hook_content_eval}")
step_variables[var_name] = hook_content_eval
else:
logger.error(f"Invalid hook format: {hook}")
def run_step_request(runner: HttpRunner, step: TStep) -> StepResult:
"""run teststep: request"""
step_result = StepResult(
name=step.name,
success=False,
)
start_time = time.time()
step.variables = runner.merge_step_variables(step.variables)
# parse
functions = runner.parser.functions_mapping
prepare_upload_step(step, functions)
request_dict = step.request.dict()
request_dict.pop("upload", None)
parsed_request_dict = runner.parser.parse_data(
request_dict, step.variables
)
parsed_request_dict["headers"].setdefault(
"HRUN-Request-ID",
f"HRUN-{runner.case_id}-{str(int(time.time() * 1000))[-6:]}",
)
step.variables["request"] = parsed_request_dict
# setup hooks
if step.setup_hooks:
call_hooks(runner, step.setup_hooks, step.variables, "setup request")
# prepare arguments
config = runner.get_config()
method = parsed_request_dict.pop("method")
url_path = parsed_request_dict.pop("url")
url = build_url(config.base_url, url_path)
parsed_request_dict["verify"] = config.verify
parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
# request
resp = runner.session.request(method, url, **parsed_request_dict)
resp_obj = ResponseObject(resp, runner.parser)
step.variables["response"] = resp_obj
# teardown hooks
if step.teardown_hooks:
call_hooks(runner, step.teardown_hooks, step.variables, "teardown request")
def log_req_resp_details():
err_msg = "\n{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)
# log request
err_msg += "====== request details ======\n"
err_msg += f"url: {url}\n"
err_msg += f"method: {method}\n"
headers = parsed_request_dict.pop("headers", {})
err_msg += f"headers: {headers}\n"
for k, v in parsed_request_dict.items():
v = utils.omit_long_data(v)
err_msg += f"{k}: {repr(v)}\n"
err_msg += "\n"
# log response
err_msg += "====== response details ======\n"
err_msg += f"status_code: {resp.status_code}\n"
err_msg += f"headers: {resp.headers}\n"
err_msg += f"body: {repr(resp.text)}\n"
logger.error(err_msg)
# extract
extractors = step.extract
extract_mapping = resp_obj.extract(extractors, step.variables)
step_result.export_vars = extract_mapping
variables_mapping = step.variables
variables_mapping.update(extract_mapping)
# validate
validators = step.validators
try:
resp_obj.validate(
validators, variables_mapping
)
step_result.success = True
except ValidationFailure:
log_req_resp_details()
raise
finally:
session_data = runner.session.data
session_data.success = step_result.success
session_data.validators = resp_obj.validation_results
# save step data
step_result.data = session_data
step_result.elapsed = time.time() - start_time
return step_result
class StepRequestValidation(IStep):
def __init__(self, step: TStep):
self.__step = step
def assert_equal(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"equal": [jmes_path, expected_value, message]}
)
return self
def assert_not_equal(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"not_equal": [jmes_path, expected_value, message]}
)
return self
def assert_greater_than(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"greater_than": [jmes_path, expected_value, message]}
)
return self
def assert_less_than(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"less_than": [jmes_path, expected_value, message]}
)
return self
def assert_greater_or_equals(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"greater_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_less_or_equals(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"less_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_length_equal(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"length_equal": [jmes_path, expected_value, message]}
)
return self
def assert_length_greater_than(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"length_greater_than": [jmes_path, expected_value, message]}
)
return self
def assert_length_less_than(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"length_less_than": [jmes_path, expected_value, message]}
)
return self
def assert_length_greater_or_equals(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"length_greater_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_length_less_or_equals(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"length_less_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_string_equals(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"string_equals": [jmes_path, expected_value, message]}
)
return self
def assert_startswith(
self, jmes_path: Text, expected_value: Text, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"startswith": [jmes_path, expected_value, message]}
)
return self
def assert_endswith(
self, jmes_path: Text, expected_value: Text, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"endswith": [jmes_path, expected_value, message]}
)
return self
def assert_regex_match(
self, jmes_path: Text, expected_value: Text, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"regex_match": [jmes_path, expected_value, message]}
)
return self
def assert_contains(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"contains": [jmes_path, expected_value, message]}
)
return self
def assert_contained_by(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"contained_by": [jmes_path, expected_value, message]}
)
return self
def assert_type_match(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step.validators.append(
{"type_match": [jmes_path, expected_value, message]}
)
return self
def struct(self) -> TStep:
return self.__step
def name(self) -> Text:
return self.__step.name
def type(self) -> Text:
return f"request-{self.__step.request.method}"
def run(self, runner: HttpRunner):
return run_step_request(runner, self.__step)
class StepRequestExtraction(IStep):
def __init__(self, step: TStep):
self.__step = step
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
self.__step.extract[var_name] = jmes_path
return self
# def with_regex(self):
# # TODO: extract response html with regex
# pass
#
# def with_jsonpath(self):
# # TODO: extract response json with jsonpath
# pass
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__step)
def struct(self) -> TStep:
return self.__step
def name(self) -> Text:
return self.__step.name
def type(self) -> Text:
return f"request-{self.__step.request.method}"
def run(self, runner: HttpRunner):
return run_step_request(runner, self.__step)
class RequestWithOptionalArgs(IStep):
def __init__(self, step: TStep):
self.__step = step
def with_params(self, **params) -> "RequestWithOptionalArgs":
self.__step.request.params.update(params)
return self
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
self.__step.request.headers.update(headers)
return self
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
self.__step.request.cookies.update(cookies)
return self
def with_data(self, data) -> "RequestWithOptionalArgs":
self.__step.request.data = data
return self
def with_json(self, req_json) -> "RequestWithOptionalArgs":
self.__step.request.req_json = req_json
return self
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
self.__step.request.timeout = timeout
return self
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
self.__step.request.verify = verify
return self
def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
self.__step.request.allow_redirects = allow_redirects
return self
def upload(self, **file_info) -> "RequestWithOptionalArgs":
self.__step.request.upload.update(file_info)
return self
def teardown_hook(
self, hook: Text, assign_var_name: Text = None
) -> "RequestWithOptionalArgs":
if assign_var_name:
self.__step.teardown_hooks.append({assign_var_name: hook})
else:
self.__step.teardown_hooks.append(hook)
return self
def extract(self) -> StepRequestExtraction:
return StepRequestExtraction(self.__step)
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__step)
def struct(self) -> TStep:
return self.__step
def name(self) -> Text:
return self.__step.name
def type(self) -> Text:
return f"request-{self.__step.request.method}"
def run(self, runner: HttpRunner):
return run_step_request(runner, self.__step)
class RunRequest(object):
def __init__(self, name: Text):
self.__step = TStep(name=name)
def with_variables(self, **variables) -> "RunRequest":
self.__step.variables.update(variables)
return self
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest":
if assign_var_name:
self.__step.setup_hooks.append({assign_var_name: hook})
else:
self.__step.setup_hooks.append(hook)
return self
def get(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.GET, url=url)
return RequestWithOptionalArgs(self.__step)
def post(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.POST, url=url)
return RequestWithOptionalArgs(self.__step)
def put(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.PUT, url=url)
return RequestWithOptionalArgs(self.__step)
def head(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.HEAD, url=url)
return RequestWithOptionalArgs(self.__step)
def delete(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.DELETE, url=url)
return RequestWithOptionalArgs(self.__step)
def options(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.OPTIONS, url=url)
return RequestWithOptionalArgs(self.__step)
def patch(self, url: Text) -> RequestWithOptionalArgs:
self.__step.request = TRequest(method=MethodEnum.PATCH, url=url)
return RequestWithOptionalArgs(self.__step)
+16
View File
@@ -0,0 +1,16 @@
import unittest
from examples.postman_echo.request_methods.request_with_functions_test import TestCaseRequestWithFunctions
class TestRunRequest(unittest.TestCase):
def test_run_request(self):
runner = TestCaseRequestWithFunctions().test_start()
summary = runner.get_summary()
self.assertTrue(summary.success)
self.assertEqual(summary.name, "request methods testcase with functions")
self.assertEqual(len(summary.step_results), 3)
self.assertEqual(summary.step_results[0].name, "get with params")
self.assertEqual(summary.step_results[1].name, "post raw text")
self.assertEqual(summary.step_results[2].name, "post form data")
+100
View File
@@ -0,0 +1,100 @@
from typing import Callable, Text
from loguru import logger
from httprunner import exceptions
from httprunner.models import IStep, StepResult, TStep, TestCaseSummary
from httprunner.runner import HttpRunner
from httprunner.step_request import call_hooks
def run_step_testcase(runner: HttpRunner, step: TStep) -> StepResult:
"""run teststep: referenced testcase"""
step_result = StepResult(name=step.name)
step_variables = step.variables
step_export = step.export
# setup hooks
if step.setup_hooks:
call_hooks(runner, step.setup_hooks, step_variables, "setup testcase")
# TODO: override testcase with current step name/variables/export
# step.testcase is a referenced testcase, e.g. RequestWithFunctions
ref_case_runner = step.testcase()
ref_case_runner.with_session(runner.session) \
.with_case_id(runner.case_id) \
.with_variables(step_variables) \
.with_export(step_export) \
.test_start()
# teardown hooks
if step.teardown_hooks:
call_hooks(runner, step.teardown_hooks, step.variables, "teardown testcase")
summary: TestCaseSummary = ref_case_runner.get_summary()
step_result.data = summary.step_results # list of step data
step_result.export_vars = summary.in_out.export_vars
step_result.success = summary.success
if step_result.export_vars:
logger.info(f"export variables: {step_result.export_vars}")
return step_result
class StepRefCase(IStep):
def __init__(self, step: TStep):
self.__step = step
def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase":
if assign_var_name:
self.__step.teardown_hooks.append({assign_var_name: hook})
else:
self.__step.teardown_hooks.append(hook)
return self
def export(self, *var_name: Text) -> "StepRefCase":
self.__step.export.extend(var_name)
return self
def struct(self) -> TStep:
return self.__step
def name(self) -> Text:
return self.__step.name
def type(self) -> Text:
return f"request-{self.__step.request.method}"
def run(self, runner: HttpRunner):
return run_step_testcase(runner, self.__step)
class RunTestCase(object):
def __init__(self, name: Text):
self.__step = TStep(name=name)
def with_variables(self, **variables) -> "RunTestCase":
self.__step.variables.update(variables)
return self
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase":
if assign_var_name:
self.__step.setup_hooks.append({assign_var_name: hook})
else:
self.__step.setup_hooks.append(hook)
return self
def call(self, testcase: Callable) -> StepRefCase:
if issubclass(testcase, HttpRunner):
# referenced testcase object
self.__step.testcase = testcase
else:
raise exceptions.ParamsError(
f"Invalid teststep referenced testcase: {testcase}"
)
return StepRefCase(self.__step)
+23
View File
@@ -0,0 +1,23 @@
import unittest
from httprunner.runner import HttpRunner
from httprunner.step_testcase import RunTestCase
from examples.postman_echo.request_methods.request_with_functions_test import TestCaseRequestWithFunctions
class TestRunTestCase(unittest.TestCase):
def setUp(self):
self.runner = HttpRunner()
def test_run_testcase_by_path(self):
step_result = RunTestCase("run referenced testcase").call(
TestCaseRequestWithFunctions
).run(self.runner)
self.assertTrue(step_result.success)
self.assertEqual(step_result.name, "run referenced testcase")
self.assertEqual(len(step_result.data), 3)
self.assertEqual(step_result.data[0].name, "get with params")
self.assertEqual(step_result.data[1].name, "post raw text")
self.assertEqual(step_result.data[2].name, "post form data")
-415
View File
@@ -1,415 +0,0 @@
import inspect
from typing import Text, Any, Union, Callable
from httprunner.models import (
TConfig,
TStep,
TRequest,
MethodEnum,
TestCase,
)
class Config(object):
def __init__(self, name: Text):
self.__name = name
self.__variables = {}
self.__base_url = ""
self.__verify = False
self.__export = []
self.__weight = 1
caller_frame = inspect.stack()[1]
self.__path = caller_frame.filename
@property
def name(self) -> Text:
return self.__name
@property
def path(self) -> Text:
return self.__path
@property
def weight(self) -> int:
return self.__weight
def variables(self, **variables) -> "Config":
self.__variables.update(variables)
return self
def base_url(self, base_url: Text) -> "Config":
self.__base_url = base_url
return self
def verify(self, verify: bool) -> "Config":
self.__verify = verify
return self
def export(self, *export_var_name: Text) -> "Config":
self.__export.extend(export_var_name)
return self
def locust_weight(self, weight: int) -> "Config":
self.__weight = weight
return self
def perform(self) -> TConfig:
return TConfig(
name=self.__name,
base_url=self.__base_url,
verify=self.__verify,
variables=self.__variables,
export=list(set(self.__export)),
path=self.__path,
weight=self.__weight,
)
class StepRequestValidation(object):
def __init__(self, step_context: TStep):
self.__step_context = step_context
def assert_equal(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"equal": [jmes_path, expected_value, message]}
)
return self
def assert_not_equal(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"not_equal": [jmes_path, expected_value, message]}
)
return self
def assert_greater_than(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"greater_than": [jmes_path, expected_value, message]}
)
return self
def assert_less_than(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"less_than": [jmes_path, expected_value, message]}
)
return self
def assert_greater_or_equals(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"greater_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_less_or_equals(
self, jmes_path: Text, expected_value: Union[int, float], message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"less_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_length_equal(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"length_equal": [jmes_path, expected_value, message]}
)
return self
def assert_length_greater_than(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"length_greater_than": [jmes_path, expected_value, message]}
)
return self
def assert_length_less_than(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"length_less_than": [jmes_path, expected_value, message]}
)
return self
def assert_length_greater_or_equals(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"length_greater_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_length_less_or_equals(
self, jmes_path: Text, expected_value: int, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"length_less_or_equals": [jmes_path, expected_value, message]}
)
return self
def assert_string_equals(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"string_equals": [jmes_path, expected_value, message]}
)
return self
def assert_startswith(
self, jmes_path: Text, expected_value: Text, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"startswith": [jmes_path, expected_value, message]}
)
return self
def assert_endswith(
self, jmes_path: Text, expected_value: Text, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"endswith": [jmes_path, expected_value, message]}
)
return self
def assert_regex_match(
self, jmes_path: Text, expected_value: Text, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"regex_match": [jmes_path, expected_value, message]}
)
return self
def assert_contains(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"contains": [jmes_path, expected_value, message]}
)
return self
def assert_contained_by(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"contained_by": [jmes_path, expected_value, message]}
)
return self
def assert_type_match(
self, jmes_path: Text, expected_value: Any, message: Text = ""
) -> "StepRequestValidation":
self.__step_context.validators.append(
{"type_match": [jmes_path, expected_value, message]}
)
return self
def perform(self) -> TStep:
return self.__step_context
class StepRequestExtraction(object):
def __init__(self, step_context: TStep):
self.__step_context = step_context
def with_jmespath(self, jmes_path: Text, var_name: Text) -> "StepRequestExtraction":
self.__step_context.extract[var_name] = jmes_path
return self
# def with_regex(self):
# # TODO: extract response html with regex
# pass
#
# def with_jsonpath(self):
# # TODO: extract response json with jsonpath
# pass
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__step_context)
def perform(self) -> TStep:
return self.__step_context
class RequestWithOptionalArgs(object):
def __init__(self, step_context: TStep):
self.__step_context = step_context
def with_params(self, **params) -> "RequestWithOptionalArgs":
self.__step_context.request.params.update(params)
return self
def with_headers(self, **headers) -> "RequestWithOptionalArgs":
self.__step_context.request.headers.update(headers)
return self
def with_cookies(self, **cookies) -> "RequestWithOptionalArgs":
self.__step_context.request.cookies.update(cookies)
return self
def with_data(self, data) -> "RequestWithOptionalArgs":
self.__step_context.request.data = data
return self
def with_json(self, req_json) -> "RequestWithOptionalArgs":
self.__step_context.request.req_json = req_json
return self
def set_timeout(self, timeout: float) -> "RequestWithOptionalArgs":
self.__step_context.request.timeout = timeout
return self
def set_verify(self, verify: bool) -> "RequestWithOptionalArgs":
self.__step_context.request.verify = verify
return self
def set_allow_redirects(self, allow_redirects: bool) -> "RequestWithOptionalArgs":
self.__step_context.request.allow_redirects = allow_redirects
return self
def upload(self, **file_info) -> "RequestWithOptionalArgs":
self.__step_context.request.upload.update(file_info)
return self
def teardown_hook(
self, hook: Text, assign_var_name: Text = None
) -> "RequestWithOptionalArgs":
if assign_var_name:
self.__step_context.teardown_hooks.append({assign_var_name: hook})
else:
self.__step_context.teardown_hooks.append(hook)
return self
def extract(self) -> StepRequestExtraction:
return StepRequestExtraction(self.__step_context)
def validate(self) -> StepRequestValidation:
return StepRequestValidation(self.__step_context)
def perform(self) -> TStep:
return self.__step_context
class RunRequest(object):
def __init__(self, name: Text):
self.__step_context = TStep(name=name)
def with_variables(self, **variables) -> "RunRequest":
self.__step_context.variables.update(variables)
return self
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunRequest":
if assign_var_name:
self.__step_context.setup_hooks.append({assign_var_name: hook})
else:
self.__step_context.setup_hooks.append(hook)
return self
def get(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.GET, url=url)
return RequestWithOptionalArgs(self.__step_context)
def post(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.POST, url=url)
return RequestWithOptionalArgs(self.__step_context)
def put(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.PUT, url=url)
return RequestWithOptionalArgs(self.__step_context)
def head(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.HEAD, url=url)
return RequestWithOptionalArgs(self.__step_context)
def delete(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.DELETE, url=url)
return RequestWithOptionalArgs(self.__step_context)
def options(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.OPTIONS, url=url)
return RequestWithOptionalArgs(self.__step_context)
def patch(self, url: Text) -> RequestWithOptionalArgs:
self.__step_context.request = TRequest(method=MethodEnum.PATCH, url=url)
return RequestWithOptionalArgs(self.__step_context)
class StepRefCase(object):
def __init__(self, step_context: TStep):
self.__step_context = step_context
def teardown_hook(self, hook: Text, assign_var_name: Text = None) -> "StepRefCase":
if assign_var_name:
self.__step_context.teardown_hooks.append({assign_var_name: hook})
else:
self.__step_context.teardown_hooks.append(hook)
return self
def export(self, *var_name: Text) -> "StepRefCase":
self.__step_context.export.extend(var_name)
return self
def perform(self) -> TStep:
return self.__step_context
class RunTestCase(object):
def __init__(self, name: Text):
self.__step_context = TStep(name=name)
def with_variables(self, **variables) -> "RunTestCase":
self.__step_context.variables.update(variables)
return self
def setup_hook(self, hook: Text, assign_var_name: Text = None) -> "RunTestCase":
if assign_var_name:
self.__step_context.setup_hooks.append({assign_var_name: hook})
else:
self.__step_context.setup_hooks.append(hook)
return self
def call(self, testcase: Callable) -> StepRefCase:
self.__step_context.testcase = testcase
return StepRefCase(self.__step_context)
def perform(self) -> TStep:
return self.__step_context
class Step(object):
def __init__(
self,
step_context: Union[
StepRequestValidation,
StepRequestExtraction,
RequestWithOptionalArgs,
RunTestCase,
StepRefCase,
],
):
self.__step_context = step_context.perform()
@property
def request(self) -> TRequest:
return self.__step_context.request
@property
def testcase(self) -> TestCase:
return self.__step_context.testcase
def perform(self) -> TStep:
return self.__step_context