Merge pull request #905 from httprunner/v3

## 3.0.5 (2020-05-22)

**Added**

- feat: each testcase has an unique id in uuid4 format
- feat: add default header `HRUN-Request-ID` for each testcase #721
- feat: builtin allure report
- feat: dump log for each testcase

**Fixed**

- fix: ensure referenced testcase share the same session

**Changed**

- change: remove default added `-s` option for hrun
This commit is contained in:
debugtalk
2020-05-22 15:20:22 +08:00
committed by GitHub
13 changed files with 159 additions and 49 deletions

View File

@@ -30,14 +30,13 @@ jobs:
poetry build poetry build
ls dist/*.whl | xargs pip install # test installation ls dist/*.whl | xargs pip install # test installation
hrun -V hrun -V
hrun run -h har2case -h
hrun startproject -h httprunner run -h
hrun har2case -h httprunner startproject -h
pip install locustio httprunner har2case -h
hrun locusts -h
- name: Run smoketest - postman echo - name: Run smoketest - postman echo
run: | run: |
hrun examples/postman_echo/request_methods hrun -s examples/postman_echo/request_methods
- name: Run smoketest - httpbin - name: Run smoketest - httpbin
run: | run: |
hrun examples/httpbin/ hrun -s examples/httpbin/

View File

@@ -1,5 +1,22 @@
# Release History # Release History
## 3.0.5 (2020-05-22)
**Added**
- feat: each testcase has an unique id in uuid4 format
- feat: add default header `HRUN-Request-ID` for each testcase #721
- feat: builtin allure report
- feat: dump log for each testcase
**Fixed**
- fix: ensure referenced testcase share the same session
**Changed**
- change: remove default added `-s` option for hrun
## 3.0.4 (2020-05-19) ## 3.0.4 (2020-05-19)
**Added** **Added**

View File

@@ -0,0 +1,2 @@

View File

@@ -1,4 +1,4 @@
__version__ = "3.0.4" __version__ = "3.0.5"
__description__ = "One-stop solution for HTTP(S) testing." __description__ = "One-stop solution for HTTP(S) testing."
from httprunner.runner import HttpRunner from httprunner.runner import HttpRunner

View File

@@ -5,9 +5,9 @@ import sys
import pytest import pytest
from loguru import logger from loguru import logger
from httprunner import __description__, __version__, exceptions from httprunner import __description__, __version__
from httprunner.ext.har2case import init_har2case_parser, main_har2case from httprunner.ext.har2case import init_har2case_parser, main_har2case
from httprunner.ext.make import init_make_parser, main_make, convert_testcase_path from httprunner.ext.make import init_make_parser, main_make
from httprunner.ext.scaffold import init_parser_scaffold, main_scaffold from httprunner.ext.scaffold import init_parser_scaffold, main_scaffold
@@ -40,9 +40,6 @@ def main_run(extra_args):
sys.exit(1) sys.exit(1)
extra_args_new.extend(testcase_path_list) extra_args_new.extend(testcase_path_list)
if "-s" not in extra_args_new:
extra_args_new.insert(0, "-s")
pytest.main(extra_args_new) pytest.main(extra_args_new)

View File

@@ -2,6 +2,8 @@ import io
import sys import sys
import unittest import unittest
import pytest
from httprunner.cli import main from httprunner.cli import main
@@ -36,3 +38,11 @@ class TestCli(unittest.TestCase):
from httprunner import __description__ from httprunner import __description__
self.assertIn(__description__, self.captured_output.getvalue().strip()) self.assertIn(__description__, self.captured_output.getvalue().strip())
def test_debug_pytest(self):
pytest.main(
[
"-s",
"examples/postman_echo/request_methods/request_with_variables_test.py",
]
)

View File

@@ -41,6 +41,7 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData:
# record actual request info # record actual request info
request_headers = dict(resp_obj.request.headers) request_headers = dict(resp_obj.request.headers)
request_cookies = dict(resp_obj.request._cookies)
request_body = resp_obj.request.body request_body = resp_obj.request.body
try: try:
request_body = json.loads(request_body) request_body = json.loads(request_body)
@@ -57,6 +58,7 @@ def get_req_resp_record(resp_obj: Response) -> ReqRespData:
method=resp_obj.request.method, method=resp_obj.request.method,
url=resp_obj.request.url, url=resp_obj.request.url,
headers=request_headers, headers=request_headers,
cookies=request_cookies,
body=request_body, body=request_body,
) )
@@ -192,7 +194,7 @@ class HttpSession(requests.Session):
logger.info( logger.info(
f"status_code: {response.status_code}, " f"status_code: {response.status_code}, "
f"response_time(ms): {response_time_ms} ms, " f"response_time(ms): {response_time_ms} ms, "
f"response_length: {content_size} bytes\n" f"response_length: {content_size} bytes"
) )
return response return response

View File

@@ -410,6 +410,11 @@ def load_project_meta(test_path: Text) -> ProjectMeta:
environments and debugtalk.py functions. environments and debugtalk.py functions.
""" """
project_meta = ProjectMeta()
if not test_path:
return project_meta
if test_path in project_meta_cached_mapping: if test_path in project_meta_cached_mapping:
return project_meta_cached_mapping[test_path] return project_meta_cached_mapping[test_path]
@@ -417,8 +422,6 @@ def load_project_meta(test_path: Text) -> ProjectMeta:
test_path test_path
) )
project_meta = ProjectMeta()
# load .env file # load .env file
# NOTICE: # NOTICE:
# environment variable maybe loaded in debugtalk.py # environment variable maybe loaded in debugtalk.py

View File

@@ -1,5 +1,7 @@
import os import os
import time import time
import uuid
import allure
from datetime import datetime from datetime import datetime
from typing import List, Dict, Text from typing import List, Dict, Text
@@ -31,11 +33,15 @@ class HttpRunner(object):
success: bool = True # indicate testcase execution result success: bool = True # indicate testcase execution result
__project_meta: ProjectMeta = None __project_meta: ProjectMeta = None
__case_id: Text = ""
__step_datas: List[StepData] = None __step_datas: List[StepData] = None
__session: HttpSession = None __session: HttpSession = None
__session_variables: VariablesMapping = {} __session_variables: VariablesMapping = {}
__start_at = 0 # time
__duration = 0 __start_at: float = 0
__duration: float = 0
# log
__log_path: Text = ""
def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner": def with_project_meta(self, project_meta: ProjectMeta) -> "HttpRunner":
self.__project_meta = project_meta self.__project_meta = project_meta
@@ -45,6 +51,10 @@ class HttpRunner(object):
self.__session = session self.__session = session
return self 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": def with_variables(self, variables: VariablesMapping) -> "HttpRunner":
self.__session_variables = variables self.__session_variables = variables
return self return self
@@ -60,6 +70,10 @@ class HttpRunner(object):
parsed_request_dict = parse_data( parsed_request_dict = parse_data(
request_dict, step.variables, self.__project_meta.functions request_dict, step.variables, self.__project_meta.functions
) )
parsed_request_dict["headers"].setdefault(
"HRUN-Request-ID",
f"HRUN-{self.__case_id}-{str(int(time.time() * 1000))[-6:]}",
)
# prepare arguments # prepare arguments
method = parsed_request_dict.pop("method") method = parsed_request_dict.pop("method")
@@ -68,7 +82,6 @@ class HttpRunner(object):
parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {}) parsed_request_dict["json"] = parsed_request_dict.pop("req_json", {})
# request # request
self.__session = self.__session or HttpSession()
resp = self.__session.request(method, url, **parsed_request_dict) resp = self.__session.request(method, url, **parsed_request_dict)
resp_obj = ResponseObject(resp) resp_obj = ResponseObject(resp)
@@ -132,6 +145,7 @@ class HttpRunner(object):
case_result = ( case_result = (
HttpRunner() HttpRunner()
.with_session(self.__session) .with_session(self.__session)
.with_case_id(self.__case_id)
.with_variables(step_variables) .with_variables(step_variables)
.run_path(ref_testcase_path) .run_path(ref_testcase_path)
) )
@@ -159,32 +173,32 @@ class HttpRunner(object):
logger.info(f"run step end: {step.name} <<<<<<\n") logger.info(f"run step end: {step.name} <<<<<<\n")
return step_data.export return step_data.export
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(self, testcase: TestCase): def run(self, testcase: TestCase):
"""main entrance""" """run testcase"""
self.config = testcase.config self.config = testcase.config
self.teststeps = testcase.teststeps self.teststeps = testcase.teststeps
self.config.variables.update(self.__session_variables)
if self.config.path: # prepare
self.__project_meta = load_project_meta(self.config.path) self.__project_meta = self.__project_meta or load_project_meta(self.config.path)
elif not self.__project_meta: self.__parse_config(self.config)
self.__project_meta = ProjectMeta()
def parse_config(config: TConfig):
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
)
parse_config(self.config)
self.__start_at = time.time() self.__start_at = time.time()
self.__step_datas: List[StepData] = [] self.__step_datas: List[StepData] = []
self.__session = self.__session or HttpSession()
self.__session_variables = {} self.__session_variables = {}
# run teststeps
for step in self.teststeps: for step in self.teststeps:
# update with config variables # update with config variables
step.variables.update(self.config.variables) step.variables.update(self.config.variables)
@@ -195,7 +209,8 @@ class HttpRunner(object):
step.variables, self.__project_meta.functions step.variables, self.__project_meta.functions
) )
# run step # run step
extract_mapping = self.__run_step(step) with allure.step(f"step: {step.name}"):
extract_mapping = self.__run_step(step)
# save extracted variables to session variables # save extracted variables to session variables
self.__session_variables.update(extract_mapping) self.__session_variables.update(extract_mapping)
@@ -231,6 +246,7 @@ class HttpRunner(object):
return TestCaseSummary( return TestCaseSummary(
name=self.config.name, name=self.config.name,
success=self.success, success=self.success,
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,
@@ -239,9 +255,36 @@ class HttpRunner(object):
in_out=TestCaseInOut( in_out=TestCaseInOut(
vars=self.config.variables, export=self.get_export_variables() vars=self.config.variables, export=self.get_export_variables()
), ),
log=self.__log_path,
step_datas=self.__step_datas, step_datas=self.__step_datas,
) )
def test_start(self): def test_start(self):
"""discovered by pytest""" """main entrance, discovered by pytest"""
return self.run(TestCase(config=self.config, teststeps=self.teststeps)) self.__case_id = self.__case_id or str(uuid.uuid4())
self.__log_path = self.__log_path or os.path.join(
"logs", f"{self.__case_id}.run.log"
)
log_handler = logger.add(self.__log_path, level="DEBUG")
# parse config name
self.__project_meta = self.__project_meta or load_project_meta(self.config.path)
variables = self.config.variables
variables.update(self.__session_variables)
self.config.name = parse_data(
self.config.name, variables, self.__project_meta.functions
)
# update allure report meta
allure.dynamic.title(self.config.name)
allure.dynamic.description(f"TestCase ID: {self.__case_id}")
logger.info(
f"Start to run testcase: {self.config.name}, TestCase ID: {self.__case_id}"
)
try:
return self.run(TestCase(config=self.config, teststeps=self.teststeps))
finally:
logger.remove(log_handler)
logger.info(f"generate testcase log: {self.__log_path}")

View File

@@ -13,6 +13,7 @@ BaseUrl = Union[HttpUrl, Text]
VariablesMapping = Dict[Text, Any] VariablesMapping = Dict[Text, Any]
FunctionsMapping = Dict[Text, Callable] FunctionsMapping = Dict[Text, Callable]
Headers = Dict[Text, Text] Headers = Dict[Text, Text]
Cookies = Dict[Text, Text]
Verify = bool Verify = bool
Hook = List[Text] Hook = List[Text]
Export = List[Text] Export = List[Text]
@@ -53,7 +54,7 @@ class Request(BaseModel):
headers: Headers = {} headers: Headers = {}
req_json: Dict = Field({}, alias="json") req_json: Dict = Field({}, alias="json")
data: Union[Text, Dict[Text, Any]] = "" data: Union[Text, Dict[Text, Any]] = ""
cookies: Dict[Text, Text] = {} cookies: Cookies = {}
timeout: int = 120 timeout: int = 120
allow_redirects: bool = True allow_redirects: bool = True
verify: Verify = False verify: Verify = False
@@ -108,15 +109,15 @@ class RequestData(BaseModel):
method: MethodEnum = MethodEnum.GET method: MethodEnum = MethodEnum.GET
url: Url url: Url
headers: Headers = {} headers: Headers = {}
# TODO: add cookies cookies: Cookies = {}
body: Union[Text, bytes, Dict, None] = {} body: Union[Text, bytes, Dict, None] = {}
class ResponseData(BaseModel): class ResponseData(BaseModel):
status_code: int status_code: int
cookies: Dict
encoding: Union[Text, None] = None
headers: Dict headers: Dict
cookies: Cookies
encoding: Union[Text, None] = None
content_type: Text content_type: Text
body: Union[Text, bytes, Dict] body: Union[Text, bytes, Dict]
@@ -147,8 +148,9 @@ class StepData(BaseModel):
class TestCaseSummary(BaseModel): class TestCaseSummary(BaseModel):
name: Text = "" name: Text
success: bool = False success: bool
case_id: Text
time: TestCaseTime time: TestCaseTime
in_out: TestCaseInOut = {} in_out: TestCaseInOut = {}
log: Text = "" log: Text = ""

36
poetry.lock generated
View File

@@ -12,6 +12,32 @@ version = "0.2.2"
python = "<3.7" python = "<3.7"
version = "2.4" version = "2.4"
[[package]]
category = "main"
description = "Allure pytest integration"
name = "allure-pytest"
optional = false
python-versions = "*"
version = "2.8.15"
[package.dependencies]
allure-python-commons = "2.8.15"
pytest = ">=4.5.0"
six = ">=1.9.0"
[[package]]
category = "main"
description = "Common module for integrate allure with python-based frameworks"
name = "allure-python-commons"
optional = false
python-versions = "*"
version = "2.8.15"
[package.dependencies]
attrs = ">=16.0.0"
pluggy = ">=0.4.0"
six = ">=1.9.0"
[[package]] [[package]]
category = "main" category = "main"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
@@ -516,7 +542,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"] testing = ["jaraco.itertools", "func-timeout"]
[metadata] [metadata]
content-hash = "be53fb0cd423bac9dda129a958a58026009a99a455081333d7af51c22a4df8cf" content-hash = "67027f8f78c61b981f3c01613ded1da2a0256a28fb92f95dd2d642b3fd1b43a5"
python-versions = "^3.6" python-versions = "^3.6"
[metadata.files] [metadata.files]
@@ -524,6 +550,14 @@ aiocontextvars = [
{file = "aiocontextvars-0.2.2-py2.py3-none-any.whl", hash = "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3"}, {file = "aiocontextvars-0.2.2-py2.py3-none-any.whl", hash = "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3"},
{file = "aiocontextvars-0.2.2.tar.gz", hash = "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"}, {file = "aiocontextvars-0.2.2.tar.gz", hash = "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"},
] ]
allure-pytest = [
{file = "allure-pytest-2.8.15.tar.gz", hash = "sha256:27f9c75194e95ba069ee2d6d2a2615ed6c7e96617ff9a492ab3a74f3f4e64be2"},
{file = "allure_pytest-2.8.15-py3-none-any.whl", hash = "sha256:62512bbce3d39b27a8e7ffbfb24e08e99c43df29b4f345168dfc9692bfddef71"},
]
allure-python-commons = [
{file = "allure-python-commons-2.8.15.tar.gz", hash = "sha256:c4768e5e1350fe2eb6e1c9dac6158dcb82e23de80c83c4fc6d71765c207c1408"},
{file = "allure_python_commons-2.8.15-py3-none-any.whl", hash = "sha256:88ad53109b6fa57e6b721f4eab59116db6037e219bf54e1f196a222ba5e2dcfe"},
]
appdirs = [ appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "3.0.4" version = "3.0.5"
description = "One-stop solution for HTTP(S) testing." description = "One-stop solution for HTTP(S) testing."
license = "Apache-2.0" license = "Apache-2.0"
readme = "README.md" readme = "README.md"
@@ -39,6 +39,7 @@ loguru = "^0.4.1"
jmespath = "^0.9.5" jmespath = "^0.9.5"
black = "^19.10b0" black = "^19.10b0"
pytest = "^5.4.2" pytest = "^5.4.2"
allure-pytest = "^2.8.15"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
coverage = "^4.5.4" coverage = "^4.5.4"