Merge pull request #427 from HttpRunner/locusts

refactor locusts
This commit is contained in:
debugtalk
2018-11-01 12:25:33 +08:00
committed by GitHub
11 changed files with 250 additions and 112 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
__title__ = 'HttpRunner' __title__ = 'HttpRunner'
__description__ = 'One-stop solution for HTTP(S) testing.' __description__ = 'One-stop solution for HTTP(S) testing.'
__url__ = 'https://github.com/HttpRunner/HttpRunner' __url__ = 'https://github.com/HttpRunner/HttpRunner'
__version__ = '1.5.14' __version__ = '1.5.15'
__author__ = 'debugtalk' __author__ = 'debugtalk'
__author_email__ = 'mail@debugtalk.com' __author_email__ = 'mail@debugtalk.com'
__license__ = 'MIT' __license__ = 'MIT'
+1 -1
View File
@@ -6,4 +6,4 @@ try:
except ImportError: except ImportError:
pass pass
from httprunner.api import HttpRunner, LocustRunner from httprunner.api import HttpRunner
-19
View File
@@ -255,22 +255,3 @@ class HttpRunner(object):
html_report_name, html_report_name,
html_report_template html_report_template
) )
class LocustRunner(object):
def __init__(self, locust_client):
self.runner = HttpRunner(http_client_session=locust_client)
def run(self, path):
try:
self.runner.run(path)
except exceptions.MyBaseError as ex:
# TODO: refactor
from locust.events import request_failure
request_failure.fire(
request_type=test.testcase_dict.get("request", {}).get("method"),
name=test.testcase_dict.get("request", {}).get("url"),
response_time=0,
exception=ex
)
+28 -7
View File
@@ -101,14 +101,12 @@ def main_hrun():
def main_locust(): def main_locust():
""" Performance test with locust: parse command line options and run commands. """ Performance test with locust: parse command line options and run commands.
""" """
logger.setup_logger("INFO")
try: try:
from httprunner import locusts from httprunner import locusts
except ImportError: except ImportError:
msg = "Locust is not installed, install first and try again.\n" msg = "Locust is not installed, install first and try again.\n"
msg += "install command: pip install locustio" msg += "install command: pip install locustio"
logger.log_warning(msg) print(msg)
exit(1) exit(1)
sys.argv[0] = 'locust' sys.argv[0] = 'locust'
@@ -119,11 +117,34 @@ def main_locust():
locusts.main() locusts.main()
sys.exit(0) sys.exit(0)
# set logging level
if "-L" in sys.argv:
loglevel_index = sys.argv.index('-L') + 1
elif "--loglevel" in sys.argv:
loglevel_index = sys.argv.index('--loglevel') + 1
else:
loglevel_index = None
if loglevel_index and loglevel_index < len(sys.argv):
loglevel = sys.argv[loglevel_index]
else:
# default
loglevel = "INFO"
logger.setup_logger(loglevel)
# get testcase file path
try: try:
testcase_index = sys.argv.index('-f') + 1 if "-f" in sys.argv:
assert testcase_index < len(sys.argv) testcase_index = sys.argv.index('-f') + 1
except (ValueError, AssertionError): elif "--locustfile" in sys.argv:
logger.log_error("Testcase file is not specified, exit.") testcase_index = sys.argv.index('--locustfile') + 1
else:
testcase_index = None
assert testcase_index and testcase_index < len(sys.argv)
except AssertionError:
print("Testcase file is not specified, exit.")
sys.exit(1) sys.exit(1)
testcase_file_path = sys.argv[testcase_index] testcase_file_path = sys.argv[testcase_index]
+7 -2
View File
@@ -14,8 +14,13 @@ class Context(object):
""" init Context with testcase variables and functions. """ init Context with testcase variables and functions.
""" """
# testcase level context # testcase level context
## TESTCASE_SHARED_VARIABLES_MAPPING and TESTCASE_SHARED_FUNCTIONS_MAPPING will not change. ## TESTCASE_SHARED_VARIABLES_MAPPING and TESTCASE_SHARED_FUNCTIONS_MAPPING are unchangeable.
self.TESTCASE_SHARED_VARIABLES_MAPPING = variables or OrderedDict() if isinstance(variables, list):
self.TESTCASE_SHARED_VARIABLES_MAPPING = utils.convert_mappinglist_to_orderdict(variables)
else:
# dict
self.TESTCASE_SHARED_VARIABLES_MAPPING = variables or OrderedDict()
self.TESTCASE_SHARED_FUNCTIONS_MAPPING = functions or OrderedDict() self.TESTCASE_SHARED_FUNCTIONS_MAPPING = functions or OrderedDict()
# testcase level request, will not change # testcase level request, will not change
+122 -53
View File
@@ -315,12 +315,70 @@ def get_module_item(module_mapping, item_type, item_name):
## testcase loader ## testcase loader
############################################################################### ###############################################################################
def _load_test_file(file_path, project_mapping): def _load_teststeps(test_block, project_mapping):
""" load testcase file or testsuite file """ load teststeps with api/testcase references
Args: Args:
file_path (str): absolute valid file path. file_path should be in the following format: test_block (dict): test block content, maybe in 3 formats.
# api reference
{
"name": "add product to cart",
"api": "api_add_cart()",
"validate": []
}
# testcase reference
{
"name": "add product to cart",
"suite": "create_and_check()",
"validate": []
}
# define directly
{
"name": "checkout cart",
"request": {},
"validate": []
}
Returns:
list: loaded teststeps list
"""
def extend_api_definition(block):
ref_call = block["api"]
def_block = _get_block_by_name(ref_call, "def-api", project_mapping)
_extend_block(block, def_block)
teststeps = []
# reference api
if "api" in test_block:
extend_api_definition(test_block)
teststeps.append(test_block)
# reference testcase
elif "suite" in test_block: # TODO: replace suite with testcase
ref_call = test_block["suite"]
block = _get_block_by_name(ref_call, "def-testcase", project_mapping)
# TODO: bugfix lost block config variables
for teststep in block["teststeps"]:
if "api" in teststep:
extend_api_definition(teststep)
teststeps.append(teststep)
# define directly
else:
teststeps.append(test_block)
return teststeps
def _load_testcase(raw_testcase, project_mapping):
""" load testcase/testsuite with api/testcase references
Args:
raw_testcase (list): raw testcase content loaded from JSON/YAML file:
[ [
# config part
{ {
"config": { "config": {
"name": "", "name": "",
@@ -328,87 +386,50 @@ def _load_test_file(file_path, project_mapping):
"request": {} "request": {}
} }
}, },
# teststeps part
{ {
"test": { "test": {...}
"name": "add product to cart",
"api": "api_add_cart()",
"validate": []
}
}, },
{ {
"test": { "test": {...}
"name": "add product to cart",
"suite": "create_and_check()",
"validate": []
}
},
{
"test": {
"name": "checkout cart",
"request": {},
"validate": []
}
} }
] ]
project_mapping (dict): project_mapping project_mapping (dict): project_mapping
Returns: Returns:
dict: testcase dict dict: loaded testcase content
{ {
"config": {}, "config": {},
"teststeps": [teststep11, teststep12] "teststeps": [teststep11, teststep12]
} }
""" """
testcase = { loaded_testcase = {
"config": {}, "config": {},
"teststeps": [] "teststeps": []
} }
for item in load_file(file_path): for item in raw_testcase:
# TODO: add json schema validation # TODO: add json schema validation
if not isinstance(item, dict) or len(item) != 1: if not isinstance(item, dict) or len(item) != 1:
raise exceptions.FileFormatError("Testcase format error: {}".format(file_path)) raise exceptions.FileFormatError("Testcase format error: {}".format(item))
key, test_block = item.popitem() key, test_block = item.popitem()
if not isinstance(test_block, dict): if not isinstance(test_block, dict):
raise exceptions.FileFormatError("Testcase format error: {}".format(file_path)) raise exceptions.FileFormatError("Testcase format error: {}".format(item))
if key == "config": if key == "config":
testcase["config"].update(test_block) loaded_testcase["config"].update(test_block)
elif key == "test": elif key == "test":
loaded_testcase["teststeps"].extend(_load_teststeps(test_block, project_mapping))
def extend_api_definition(block):
ref_call = block["api"]
def_block = _get_block_by_name(ref_call, "def-api", project_mapping)
_extend_block(block, def_block)
# reference api
if "api" in test_block:
extend_api_definition(test_block)
testcase["teststeps"].append(test_block)
# reference testcase
elif "suite" in test_block: # TODO: replace suite with testcase
ref_call = test_block["suite"]
block = _get_block_by_name(ref_call, "def-testcase", project_mapping)
# TODO: bugfix lost block config variables
for teststep in block["teststeps"]:
if "api" in teststep:
extend_api_definition(teststep)
testcase["teststeps"].append(teststep)
# define directly
else:
testcase["teststeps"].append(test_block)
else: else:
logger.log_warning( logger.log_warning(
"unexpected block key: {}. block key should only be 'config' or 'test'.".format(key) "unexpected block key: {}. block key should only be 'config' or 'test'.".format(key)
) )
return testcase return loaded_testcase
def _get_block_by_name(ref_call, ref_type, project_mapping): def _get_block_by_name(ref_call, ref_type, project_mapping):
@@ -917,7 +938,7 @@ def load_tests(path, dot_env_path=None):
"teststeps": [ "teststeps": [
# teststep data structure # teststep data structure
{ {
'name': 'test step desc2', 'name': 'test step desc1',
'variables': [], # optional 'variables': [], # optional
'extract': [], # optional 'extract': [], # optional
'validate': [], 'validate': [],
@@ -956,8 +977,9 @@ def load_tests(path, dot_env_path=None):
elif os.path.isfile(path): elif os.path.isfile(path):
try: try:
raw_testcase = load_file(path)
project_mapping = load_project_tests(path, dot_env_path) project_mapping = load_project_tests(path, dot_env_path)
testcase = _load_test_file(path, project_mapping) testcase = _load_testcase(raw_testcase, project_mapping)
testcase["config"]["path"] = path testcase["config"]["path"] = path
testcase["config"]["refs"] = project_mapping testcase["config"]["refs"] = project_mapping
testcases_list = [testcase] testcases_list = [testcase]
@@ -965,3 +987,50 @@ def load_tests(path, dot_env_path=None):
testcases_list = [] testcases_list = []
return testcases_list return testcases_list
def load_locust_tests(path, dot_env_path=None):
""" load locust testcases
Args:
path (str): testcase/testsuite file path.
dot_env_path (str): specified .env file path
Returns:
dict: locust testcases with weight
{
"config": {...},
"tests": [
# weight 3
[teststep11],
[teststep11],
[teststep11],
# weight 2
[teststep21, teststep22],
[teststep21, teststep22]
]
}
"""
raw_testcase = load_file(path)
project_mapping = load_project_tests(path, dot_env_path)
config = {
"refs": project_mapping
}
tests = []
for item in raw_testcase:
key, test_block = item.popitem()
if key == "config":
config.update(test_block)
elif key == "test":
teststeps = _load_teststeps(test_block, project_mapping)
weight = test_block.pop("weight", 1)
for _ in range(weight):
tests.append(teststeps)
return {
"config": config,
"tests": tests
}
-3
View File
@@ -40,13 +40,10 @@ def gen_locustfile(testcase_file_path):
"templates", "templates",
"locustfile_template" "locustfile_template"
) )
testcases = loader.load_tests(testcase_file_path)
host = testcases[0].get("config", {}).get("request", {}).get("base_url", "")
with io.open(template_path, encoding='utf-8') as template: with io.open(template_path, encoding='utf-8') as template:
with io.open(locustfile_path, 'w', encoding='utf-8') as locustfile: with io.open(locustfile_path, 'w', encoding='utf-8') as locustfile:
template_content = template.read() template_content = template.read()
template_content = template_content.replace("$HOST", host)
template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path) template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path)
locustfile.write(template_content) locustfile.write(template_content)
+33 -11
View File
@@ -1,24 +1,46 @@
#coding: utf-8 import logging
import random
import zmq import zmq
from httprunner.exceptions import MyBaseError, MyBaseFailure
from httprunner.loader import load_locust_tests
from httprunner.runner import Runner
from locust import HttpLocust, TaskSet, task from locust import HttpLocust, TaskSet, task
from httprunner import LocustRunner from locust.events import request_failure
from httprunner.loader import load_tests
logging.getLogger().setLevel(logging.CRITICAL)
logging.getLogger('locust.main').setLevel(logging.INFO)
logging.getLogger('locust.runners').setLevel(logging.INFO)
class WebPageTasks(TaskSet): class WebPageTasks(TaskSet):
def on_start(self): def on_start(self):
self.test_runner = LocustRunner(self.client) self.test_runner = Runner(self.locust.config, self.client)
self.testcases = loader.load_tests(self.locust.file_path) self.testcases = load_locust_tests(self.locust.file_path)
@task @task(weight=1)
def test_specified_scenario(self): def test_any(self):
self.test_runner.run(self.testcases) teststeps = random.choice(self.locust.tests)
for teststep in teststeps:
try:
self.test_runner.run_test(teststep)
except (MyBaseError, MyBaseFailure) as ex:
request_failure.fire(
request_type=teststep.get("request", {}).get("method"),
name=teststep.get("name"),
response_time=0,
exception=ex
)
class WebPageUser(HttpLocust): class WebPageUser(HttpLocust):
host = "$HOST"
task_set = WebPageTasks task_set = WebPageTasks
min_wait = 1000 min_wait = 10
max_wait = 5000 max_wait = 30
file_path = "$TESTCASE_FILE" file_path = "$TESTCASE_FILE"
locust_tests = load_locust_tests(file_path)
config = locust_tests["config"]
tests = locust_tests["tests"]
host = config.get('request', {}).get('base_url', '')
+34
View File
@@ -0,0 +1,34 @@
- config:
name: basic test with httpbin
request:
base_url: https://httpbin.org/
- test:
name: index
weight: 5
request:
url: /
method: GET
validate:
- eq: ["status_code", 200]
- contains: [content, "HTTP Request &amp; Response Service"]
- test:
name: headers
weight: 3
request:
url: /headers
method: GET
validate:
- eq: ["status_code", 200]
- eq: [content.headers.Host, "httpbin.org"]
- test:
name: user-agent
weight: 2
request:
url: /user-agent
method: GET
validate:
- eq: ["status_code", 200]
- startswith: [content.user-agent, "python-requests"]
+1 -13
View File
@@ -2,7 +2,7 @@ import os
import shutil import shutil
import time import time
from httprunner import HttpRunner, LocustRunner, api, loader, parser from httprunner import HttpRunner, api, loader, parser
from locust import HttpLocust from locust import HttpLocust
from tests.api_server import HTTPBIN_SERVER from tests.api_server import HTTPBIN_SERVER
from tests.base import ApiServerUnittest from tests.base import ApiServerUnittest
@@ -375,15 +375,3 @@ class TestHttpRunner(ApiServerUnittest):
os.getcwd(), 'tests/httpbin/basic.yml') os.getcwd(), 'tests/httpbin/basic.yml')
runner = HttpRunner().run(testcase_file_path) runner = HttpRunner().run(testcase_file_path)
self.assertTrue(runner.summary["success"]) self.assertTrue(runner.summary["success"])
class TestLocustRunner(ApiServerUnittest):
def setUp(self):
WebPageUser = type('WebPageUser', (HttpLocust,), {})
self.locust_client = WebPageUser.client
def test_LocustRunner(self):
testcase_file = os.path.join(os.getcwd(), 'tests', 'httpbin', 'basic.yml')
locust_runner = LocustRunner(self.locust_client)
locust_runner.run(testcase_file)
+23 -2
View File
@@ -283,8 +283,20 @@ class TestSuiteLoader(unittest.TestCase):
def setUpClass(cls): def setUpClass(cls):
cls.project_mapping = loader.load_project_tests(os.path.join(os.getcwd(), "tests")) cls.project_mapping = loader.load_project_tests(os.path.join(os.getcwd(), "tests"))
def test_load_test_file_testcase(self): def test_load_teststeps(self):
testcase = loader._load_test_file("tests/testcases/smoketest.yml", self.project_mapping) test_block = {
"name": "setup and reset all.",
"suite": "setup_and_reset($device_sn)",
"output": ["token", "device_sn"]
}
teststeps = loader._load_teststeps(test_block, self.project_mapping)
self.assertEqual(len(teststeps), 2)
self.assertEqual(teststeps[0]["name"], "get token")
self.assertEqual(teststeps[1]["name"], "reset all users")
def test_load_testcase(self):
raw_testcase = loader.load_file("tests/testcases/smoketest.yml")
testcase = loader._load_testcase(raw_testcase, self.project_mapping)
self.assertEqual(testcase["config"]["name"], "smoketest") self.assertEqual(testcase["config"]["name"], "smoketest")
self.assertIn("device_sn", testcase["config"]["variables"][0]) self.assertIn("device_sn", testcase["config"]["variables"][0])
self.assertEqual(len(testcase["teststeps"]), 8) self.assertEqual(len(testcase["teststeps"]), 8)
@@ -541,3 +553,12 @@ class TestSuiteLoader(unittest.TestCase):
self.assertIn("get_token", project_mapping["def-api"]) self.assertIn("get_token", project_mapping["def-api"])
self.assertIn("setup_and_reset", project_mapping["def-testcase"]) self.assertIn("setup_and_reset", project_mapping["def-testcase"])
self.assertEqual(project_mapping["env"]["PROJECT_KEY"], "ABCDEFGH") self.assertEqual(project_mapping["env"]["PROJECT_KEY"], "ABCDEFGH")
def test_load_locust_tests(self):
path = os.path.join(
os.getcwd(), 'tests/data/demo_locust.yml')
locust_tests = loader.load_locust_tests(path)
self.assertEqual(locust_tests["config"]["refs"]["env"]["UserName"], "debugtalk")
self.assertEqual(len(locust_tests["tests"]), 10)
self.assertEqual(locust_tests["tests"][0][0]["name"], "index")
self.assertEqual(locust_tests["tests"][9][0]["name"], "user-agent")