Merge pull request #897 from httprunner/v3

## 3.0.3 (2020-05-17)

**Fixed**

- fix: compatibility with testcase file path includes dots, space and minus sign
- fix: testcase generator, validate content.xxx => body.xxx
- fix: scaffold for v3
This commit is contained in:
debugtalk
2020-05-18 10:47:56 +08:00
committed by GitHub
21 changed files with 172 additions and 91 deletions

View File

@@ -1,5 +1,13 @@
# Release History # Release History
## 3.0.3 (2020-05-17)
**Fixed**
- fix: compatibility with testcase file path includes dots, space and minus sign
- fix: testcase generator, validate content.xxx => body.xxx
- fix: scaffold for v3
## 3.0.2 (2020-05-16) ## 3.0.2 (2020-05-16)
**Added** **Added**

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/httpbin/basic.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/httpbin/hooks.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/httpbin/load_image.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/httpbin/upload.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/httpbin/validate.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/postman_echo/request_methods/hardcode.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/postman_echo/request_methods/request_with_functions.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/postman_echo/request_methods/request_with_testcase_reference.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/postman_echo/request_methods/request_with_variables.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/postman_echo/request_methods/validate_with_functions.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,5 @@
# NOTICE: Generated By HttpRunner. DO'NOT EDIT! # NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: examples/postman_echo/request_methods/validate_with_variables.yml
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep

View File

@@ -1,4 +1,4 @@
__version__ = "3.0.2" __version__ = "3.0.3"
__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

@@ -25,7 +25,7 @@ def main_run(extra_args):
continue continue
elif os.path.isfile(item): elif os.path.isfile(item):
# replace YAML/JSON file path with generated python file # replace YAML/JSON file path with generated python file
extra_args[index] = convert_testcase_path(item) extra_args[index], _ = convert_testcase_path(item)
tests_path_list.append(item) tests_path_list.append(item)

View File

@@ -261,9 +261,7 @@ class HarParser(object):
if isinstance(value, (dict, list)): if isinstance(value, (dict, list)):
continue continue
teststep_dict["validate"].append( teststep_dict["validate"].append({"eq": ["body.{}".format(key), value]})
{"eq": ["content.{}".format(key), value]}
)
def _prepare_teststep(self, entry_json): def _prepare_teststep(self, entry_json):
""" extract info from entry dict and make teststep """ extract info from entry dict and make teststep
@@ -299,7 +297,7 @@ class HarParser(object):
def _prepare_config(self): def _prepare_config(self):
""" prepare config block. """ prepare config block.
""" """
return {"name": "testcase description", "variables": {}} return {"name": "testcase description", "variables": {}, "verify": False}
def _prepare_teststeps(self): def _prepare_teststeps(self):
""" make teststep list. """ make teststep list.

View File

@@ -22,9 +22,9 @@ class TestHar(TestUtils):
for validator in teststep_dict["validate"] for validator in teststep_dict["validate"]
} }
self.assertEqual(validators_mapping["status_code"], 200) self.assertEqual(validators_mapping["status_code"], 200)
self.assertEqual(validators_mapping["content.IsSuccess"], True) self.assertEqual(validators_mapping["body.IsSuccess"], True)
self.assertEqual(validators_mapping["content.Code"], 200) self.assertEqual(validators_mapping["body.Code"], 200)
self.assertEqual(validators_mapping["content.Message"], None) self.assertEqual(validators_mapping["body.Message"], None)
def test_prepare_teststeps(self): def test_prepare_teststeps(self):
teststeps = self.har_parser._prepare_teststeps() teststeps = self.har_parser._prepare_teststeps()

View File

@@ -1,6 +1,6 @@
import os import os
import subprocess import subprocess
from typing import Union, Text, List from typing import Union, Text, List, Tuple
import jinja2 import jinja2
from loguru import logger from loguru import logger
@@ -10,6 +10,7 @@ from httprunner.exceptions import TestCaseFormatError
from httprunner.loader import load_testcase_file, load_folder_files from httprunner.loader import load_testcase_file, load_folder_files
__TMPL__ = """# NOTICE: Generated By HttpRunner. DO'NOT EDIT! __TMPL__ = """# NOTICE: Generated By HttpRunner. DO'NOT EDIT!
# FROM: {{ testcase_path }}
from httprunner import HttpRunner, TConfig, TStep from httprunner import HttpRunner, TConfig, TStep
@@ -28,6 +29,28 @@ if __name__ == "__main__":
""" """
def convert_testcase_path(testcase_path: Text) -> Tuple[Text, Text]:
"""convert single YAML/JSON testcase path to python file"""
if os.path.isdir(testcase_path):
# folder does not need to convert
return testcase_path, ""
raw_file_name, file_suffix = os.path.splitext(os.path.basename(testcase_path))
file_suffix = file_suffix.lower()
if file_suffix not in [".json", ".yml", ".yaml"]:
raise exceptions.ParamsError("")
file_name = raw_file_name.replace(" ", "_").replace(".", "_").replace("-", "_")
testcase_dir = os.path.dirname(testcase_path)
testcase_python_path = os.path.join(testcase_dir, f"{file_name}_test.py")
# convert title case, e.g. request_with_variables => RequestWithVariables
name_in_title_case = file_name.title().replace("_", "")
return testcase_python_path, name_in_title_case
def make_testcase(testcase_path: str) -> Union[str, None]: def make_testcase(testcase_path: str) -> Union[str, None]:
logger.info(f"start to make testcase: {testcase_path}") logger.info(f"start to make testcase: {testcase_path}")
try: try:
@@ -37,16 +60,12 @@ def make_testcase(testcase_path: str) -> Union[str, None]:
template = jinja2.Template(__TMPL__) template = jinja2.Template(__TMPL__)
raw_file_name, _ = os.path.splitext(os.path.basename(testcase_path)) testcase_python_path, name_in_title_case = convert_testcase_path(testcase_path)
# convert title case, e.g. request_with_variables => RequestWithVariables
name_in_title_case = raw_file_name.title().replace("_", "")
testcase_dir = os.path.dirname(testcase_path)
testcase_python_path = os.path.join(testcase_dir, f"{raw_file_name}_test.py")
config = testcase["config"] config = testcase["config"]
config["path"] = testcase_python_path config["path"] = testcase_python_path
data = { data = {
"testcase_path": testcase_path,
"class_name": f"TestCase{name_in_title_case}", "class_name": f"TestCase{name_in_title_case}",
"config": config, "config": config,
"teststeps": testcase["teststeps"], "teststeps": testcase["teststeps"],
@@ -60,26 +79,10 @@ def make_testcase(testcase_path: str) -> Union[str, None]:
return testcase_python_path return testcase_python_path
def convert_testcase_path(testcase_path: Text) -> Text:
"""convert single YAML/JSON testcase path to python file"""
if os.path.isdir(testcase_path):
# folder does not need to convert
return testcase_path
file_suffix = os.path.splitext(testcase_path)[1].lower()
if file_suffix == ".json":
return testcase_path.replace(".json", "_test.py")
elif file_suffix == ".yaml":
return testcase_path.replace(".yaml", "_test.py")
elif file_suffix == ".yml":
return testcase_path.replace(".yml", "_test.py")
else:
raise exceptions.ParamsError("")
def format_with_black(tests_path: Text): def format_with_black(tests_path: Text):
logger.info("format testcases with black ...") logger.info("format testcases with black ...")
tests_path = convert_testcase_path(tests_path) tests_path, _ = convert_testcase_path(tests_path)
try: try:
subprocess.run(["black", tests_path]) subprocess.run(["black", tests_path])
except subprocess.CalledProcessError as ex: except subprocess.CalledProcessError as ex:
@@ -119,7 +122,7 @@ def init_make_parser(subparsers):
""" make testcases: parse command line options and run commands. """ make testcases: parse command line options and run commands.
""" """
parser = subparsers.add_parser( parser = subparsers.add_parser(
"make", help="Convert YAML/JSON testcases to Python unittests.", "make", help="Convert YAML/JSON testcases to pytest cases.",
) )
parser.add_argument( parser.add_argument(
"testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path" "testcase_path", nargs="*", help="Specify YAML/JSON testcase file/folder path"

View File

@@ -1,5 +1,5 @@
import unittest import unittest
from httprunner.ext.make import make_testcase, main_make from httprunner.ext.make import make_testcase, main_make, convert_testcase_path
class TestLoader(unittest.TestCase): class TestLoader(unittest.TestCase):
@@ -18,3 +18,45 @@ class TestLoader(unittest.TestCase):
"examples/postman_echo/request_methods/request_with_functions_test.py", "examples/postman_echo/request_methods/request_with_functions_test.py",
testcase_python_list, testcase_python_list,
) )
def test_convert_testcase_path(self):
self.assertEqual(
convert_testcase_path("mubu.login.yml")[0],
"mubu_login_test.py"
)
self.assertEqual(
convert_testcase_path("/path/to/mubu.login.yml")[0],
"/path/to/mubu_login_test.py"
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu.login.yml")[0],
"/path/to 2/mubu_login_test.py"
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu.login.yml")[1],
"MubuLogin"
)
self.assertEqual(
convert_testcase_path("mubu login.yml")[0],
"mubu_login_test.py"
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu login.yml")[1],
"MubuLogin"
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu-login.yml")[0],
"/path/to 2/mubu_login_test.py"
)
self.assertEqual(
convert_testcase_path("/path/to 2/mubu-login.yml")[1],
"MubuLogin"
)
self.assertEqual(
convert_testcase_path("/path/to 2/幕布login.yml")[0],
"/path/to 2/幕布login_test.py"
)
self.assertEqual(
convert_testcase_path("/path/to/幕布login.yml")[1],
"幕布Login"
)

View File

@@ -37,71 +37,84 @@ def create_scaffold(project_name):
msg = f"created file: {path}" msg = f"created file: {path}"
logger.info(msg) logger.info(msg)
demo_api_content = """ demo_testcase_request_content = """
name: demo api
variables:
var1: value1
var2: value2
request:
url: /api/path/$var1
method: POST
headers:
Content-Type: "application/json"
json:
key: $var2
validate:
- eq: ["status_code", 200]
"""
demo_testcase_content = """
config: config:
name: "demo testcase" name: "request methods testcase with functions"
variables: variables:
device_sn: "ABC" foo1: session_bar1
username: ${ENV(USERNAME)} base_url: "https://postman-echo.com"
password: ${ENV(PASSWORD)} verify: False
base_url: "http://127.0.0.1:5000"
teststeps: teststeps:
- -
name: demo step 1 name: get with params
api: path/to/api1.yml
variables: variables:
user_agent: 'iOS/10.3' foo1: bar1
device_sn: $device_sn foo2: session_bar2
sum_v: "${sum_two(1, 2)}"
request:
method: GET
url: /get
params:
foo1: $foo1
foo2: $foo2
sum_v: $sum_v
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
extract: extract:
token: content.token session_foo2: "body.args.foo2"
validate: validate:
- eq: ["status_code", 200] - eq: ["status_code", 200]
- eq: ["body.args.foo1", "session_bar1"]
- eq: ["body.args.sum_v", 3]
- eq: ["body.args.foo2", "session_bar2"]
- -
name: demo step 2 name: post raw text
api: path/to/api2.yml
variables: variables:
token: $token foo1: "hello world"
foo3: "$session_foo2"
request:
method: POST
url: /post
headers:
User-Agent: HttpRunner/${get_httprunner_version()}
Content-Type: "text/plain"
data: "This is expected to be sent back as part of response body: $foo1-$foo3."
validate:
- eq: ["status_code", 200]
- eq: ["body.data", "This is expected to be sent back as part of response body: session_bar1-session_bar2."]
""" """
demo_testsuite_content = """ demo_testcase_with_ref_content = """
config: config:
name: "demo testsuite" name: "request methods testcase: reference testcase"
variables: variables:
device_sn: "XYZ" foo1: session_bar1
base_url: "http://127.0.0.1:5000" base_url: "https://postman-echo.com"
verify: False
testcases: teststeps:
- -
name: call demo_testcase with data 1 name: request with referenced testcase
testcase: path/to/demo_testcase.yml
variables: variables:
device_sn: $device_sn foo1: override_bar1
- # NOTICE: relative testcase path based on debugtalk.py
name: call demo_testcase with data 2 testcase: testcases/demo_testcase_request.yml
testcase: path/to/demo_testcase.yml
variables:
device_sn: $device_sn
""" """
ignore_content = "\n".join( ignore_content = "\n".join(
[".env", "reports/*", "__pycache__/*", "*.pyc", ".python-version", "logs/*"] [".env", "reports/*", "__pycache__/*", "*.pyc", ".python-version", "logs/*"]
) )
demo_debugtalk_content = """ demo_debugtalk_content = """import time
import time
from httprunner import __version__
def get_httprunner_version():
return __version__
def sum_two(m, n):
return m + n
def sleep(n_secs): def sleep(n_secs):
time.sleep(n_secs) time.sleep(n_secs)
@@ -109,18 +122,17 @@ def sleep(n_secs):
demo_env_content = "\n".join(["USERNAME=leolee", "PASSWORD=123456"]) demo_env_content = "\n".join(["USERNAME=leolee", "PASSWORD=123456"])
create_folder(project_name) create_folder(project_name)
create_folder(os.path.join(project_name, "api")) create_folder(os.path.join(project_name, "har"))
create_folder(os.path.join(project_name, "testcases")) create_folder(os.path.join(project_name, "testcases"))
create_folder(os.path.join(project_name, "testsuites"))
create_folder(os.path.join(project_name, "reports")) create_folder(os.path.join(project_name, "reports"))
create_file(os.path.join(project_name, "api", "demo_api.yml"), demo_api_content)
create_file( create_file(
os.path.join(project_name, "testcases", "demo_testcase.yml"), os.path.join(project_name, "testcases", "demo_testcase_request.yml"),
demo_testcase_content, demo_testcase_request_content,
) )
create_file( create_file(
os.path.join(project_name, "testsuites", "demo_testsuite.yml"), os.path.join(project_name, "testcases", "demo_testcase_ref.yml"),
demo_testsuite_content, demo_testcase_with_ref_content,
) )
create_file(os.path.join(project_name, "debugtalk.py"), demo_debugtalk_content) create_file(os.path.join(project_name, "debugtalk.py"), demo_debugtalk_content)
create_file(os.path.join(project_name, ".env"), demo_env_content) create_file(os.path.join(project_name, ".env"), demo_env_content)

View File

@@ -1,5 +1,6 @@
import os import os
import shutil import shutil
import subprocess
import unittest import unittest
from httprunner.ext.scaffold import create_scaffold from httprunner.ext.scaffold import create_scaffold
@@ -9,10 +10,16 @@ class TestUtils(unittest.TestCase):
def test_create_scaffold(self): def test_create_scaffold(self):
project_name = "projectABC" project_name = "projectABC"
create_scaffold(project_name) create_scaffold(project_name)
self.assertTrue(os.path.isdir(os.path.join(project_name, "api"))) self.assertTrue(os.path.isdir(os.path.join(project_name, "har")))
self.assertTrue(os.path.isdir(os.path.join(project_name, "testcases"))) self.assertTrue(os.path.isdir(os.path.join(project_name, "testcases")))
self.assertTrue(os.path.isdir(os.path.join(project_name, "testsuites")))
self.assertTrue(os.path.isdir(os.path.join(project_name, "reports"))) self.assertTrue(os.path.isdir(os.path.join(project_name, "reports")))
self.assertTrue(os.path.isfile(os.path.join(project_name, "debugtalk.py"))) self.assertTrue(os.path.isfile(os.path.join(project_name, "debugtalk.py")))
self.assertTrue(os.path.isfile(os.path.join(project_name, ".env"))) self.assertTrue(os.path.isfile(os.path.join(project_name, ".env")))
shutil.rmtree(project_name)
# run demo testcases
try:
subprocess.check_call(["hrun", project_name])
except subprocess.SubprocessError:
raise
finally:
shutil.rmtree(project_name)

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "httprunner" name = "httprunner"
version = "3.0.2" version = "3.0.3"
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"