Skip to content

Commit

Permalink
new feature: support parameters and data driven
Browse files Browse the repository at this point in the history
  • Loading branch information
httprunner committed Feb 15, 2018
1 parent 1db17cb commit f8569aa
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 7 deletions.
2 changes: 1 addition & 1 deletion httprunner/__init__.py
@@ -1 +1 @@
__version__ = '0.9.1'
__version__ = '0.9.2'
19 changes: 16 additions & 3 deletions httprunner/task.py
Expand Up @@ -49,13 +49,26 @@ def __init__(self, testset, variables_mapping=None, http_client_session=None):
super(ApiTestSuite, self).__init__()

self.config_dict = testset.get("config", {})

variables = self.config_dict.get("variables", [])
variables_mapping = variables_mapping or {}
self.config_dict["variables"] = utils.override_variables_binds(variables, variables_mapping)

self.test_runner = runner.Runner(self.config_dict, http_client_session)
testcases = testset.get("testcases", [])
self._add_tests_to_suite(testcases)
parameters = self.config_dict.get("parameters", [])
cartesian_product_parameters = testcase.gen_cartesian_product_parameters(
parameters,
self.config_dict["path"]
) or [{}]
for parameter_mapping in cartesian_product_parameters:
if parameter_mapping:
self.config_dict["variables"] = utils.override_variables_binds(
self.config_dict["variables"],
parameter_mapping
)

self.test_runner = runner.Runner(self.config_dict, http_client_session)
testcases = testset.get("testcases", [])
self._add_tests_to_suite(testcases)

def _add_tests_to_suite(self, testcases):
for testcase_dict in testcases:
Expand Down
123 changes: 122 additions & 1 deletion httprunner/testcase.py
@@ -1,8 +1,10 @@
import ast
import io
import itertools
import json
import logging
import os
import random
import re
from collections import OrderedDict

Expand Down Expand Up @@ -42,12 +44,70 @@ def _load_json_file(json_file):
check_format(json_file, json_content)
return json_content

def _load_csv_file(csv_file):
""" load csv file and check file content format
@param
csv_file: csv file path
e.g. csv file content:
username,password
test1,111111
test2,222222
test3,333333
@return
list of parameter, each parameter is in dict format
e.g.
[
{'username': 'test1', 'password': '111111'},
{'username': 'test2', 'password': '222222'},
{'username': 'test3', 'password': '333333'}
]
"""
csv_content_list = []
parameter_list = None
collums_num = 0
with io.open(csv_file, encoding='utf-8') as data_file:
for line in data_file:
line_data = line.strip().split(",")
if line_data == [""]:
# ignore empty line
continue

if not parameter_list:
# first line will always be parameter name
expected_filename = "{}.csv".format("-".join(line_data))
if not csv_file.endswith(expected_filename):
raise exception.FileFormatError("CSV file name does not match with headers: {}".format(csv_file))

parameter_list = line_data
collums_num = len(parameter_list)
continue

# from the second line
if len(line_data) != collums_num:
err_msg = "CSV file collums does match with headers.\n"
err_msg += "\tcsv file path: {}\n".format(csv_file)
err_msg += "\terror line content: {}".format(line_data)
raise exception.FileFormatError(err_msg)
else:
data = {}
for index, parameter_name in enumerate(parameter_list):
data[parameter_name] = line_data[index]

csv_content_list.append(data)

return csv_content_list

def load_file(file_path):
file_suffix = os.path.splitext(file_path)[1]
if not os.path.isfile(file_path):
raise exception.FileNotFoundError("{} does not exist.".format(file_path))

file_suffix = os.path.splitext(file_path)[1].lower()
if file_suffix == '.json':
return _load_json_file(file_path)
elif file_suffix in ['.yaml', '.yml']:
return _load_yaml_file(file_path)
elif file_suffix == ".csv":
return _load_csv_file(file_path)
else:
# '' or other suffix
err_msg = u"file is not in YAML/JSON format: {}".format(file_path)
Expand Down Expand Up @@ -550,6 +610,67 @@ def check_format(file_path, content):
logging.error(err_msg)
raise exception.FileFormatError(err_msg)

def gen_cartesian_product(*args):
""" generate cartesian product for lists
@param
(list) args
[{"a": 1}, {"a": 2}],
[
{"x": 111, "y": 112},
{"x": 121, "y": 122}
]
@return
cartesian product in list
[
{'a': 1, 'x': 111, 'y': 112},
{'a': 1, 'x': 121, 'y': 122},
{'a': 2, 'x': 111, 'y': 112},
{'a': 2, 'x': 121, 'y': 122}
]
"""
if not args:
return []
elif len(args) == 1:
return args[0]

product_list = []
for product_item_tuple in itertools.product(*args):
product_item_dict = {}
for item in product_item_tuple:
product_item_dict.update(item)

product_list.append(product_item_dict)

return product_list

def gen_cartesian_product_parameters(parameters, testset_path):
""" parse parameters and generate cartesian product
@params
(list) parameters: parameter name and fetch method
e.g.
[
{"user_agent": "Random"},
{"app_version": "Sequential"}
]
(str) testset_path: testset file path, used for locating csv file
@return cartesian product in list
"""
parameters_content_list = []
for parameter in parameters:
parameter_name, fetch_method = list(parameter.items())[0]
parameter_file_path = os.path.join(
os.path.dirname(testset_path),
"{}.csv".format(parameter_name)
)
csv_content_list = load_file(parameter_file_path)

if fetch_method.lower() == "random":
random.shuffle(csv_content_list)

parameters_content_list.append(csv_content_list)

return gen_cartesian_product(*parameters_content_list)


class TestcaseParser(object):

Expand Down
4 changes: 4 additions & 0 deletions tests/data/app_version.csv
@@ -0,0 +1,4 @@
app_version
2.8.5
2.8.6

26 changes: 26 additions & 0 deletions tests/data/demo_parameters.yml
@@ -0,0 +1,26 @@
- config:
name: "user management testset."
parameters:
- user_agent: Random
- app_version: Sequential
variables:
- user_agent: 'iOS/10.3'
- device_sn: ${gen_random_string(15)}
- os_platform: 'ios'
- app_version: '2.8.6'
request:
base_url: $BASE_URL
headers:
Content-Type: application/json
device_sn: $device_sn
output:
- token

- test:
name: get token with $user_agent and $app_version
api: get_token($user_agent, $device_sn, $os_platform, $app_version)
extract:
- token: content.token
validate:
- "eq": ["status_code", 200]
- "len_eq": ["content.token", 16]
4 changes: 4 additions & 0 deletions tests/data/user_agent.csv
@@ -0,0 +1,4 @@
user_agent
iOS/10.1
iOS/10.2
iOS/10.3
4 changes: 4 additions & 0 deletions tests/data/username-password.csv
@@ -0,0 +1,4 @@
username,password
test1,111111
test2,222222
test3,333333
8 changes: 8 additions & 0 deletions tests/test_runner.py
Expand Up @@ -158,3 +158,11 @@ def test_bugfix_type_match(self):

test = testcases[2]["test"]
self.assertTrue(self.test_runner._run_test(test))

def test_run_testset_with_parameters(self):
testcase_file_path = os.path.join(
os.getcwd(), 'tests/data/demo_parameters.yml')
result = run_suite_path(testcase_file_path)
self.assertTrue(result.success)
self.assertIn("token", result.output)
self.assertEqual(result.stat.total, 6)
111 changes: 109 additions & 2 deletions tests/test_testcase.py
Expand Up @@ -3,14 +3,16 @@
import unittest

from httprunner import testcase
from httprunner.exception import ApiNotFound, FileFormatError, ParamsError
from httprunner.exception import (ApiNotFound, FileFormatError,
FileNotFoundError, ParamsError)


class TestcaseParserUnittest(unittest.TestCase):

def test_load_testcases_bad_filepath(self):
testcase_file_path = os.path.join(os.getcwd(), 'tests/data/demo')
self.assertEqual(testcase.load_file(testcase_file_path), [])
with self.assertRaises(FileNotFoundError):
testcase.load_file(testcase_file_path)

def test_load_json_testcases(self):
testcase_file_path = os.path.join(
Expand All @@ -34,6 +36,111 @@ def test_load_yaml_testcases(self):
self.assertIn('url', test['request'])
self.assertIn('method', test['request'])

def test_load_csv_file_one_parameter(self):
csv_file_path = os.path.join(
os.getcwd(), 'tests/data/user_agent.csv')
csv_content = testcase.load_file(csv_file_path)
self.assertEqual(
csv_content,
[
{'user_agent': 'iOS/10.1'},
{'user_agent': 'iOS/10.2'},
{'user_agent': 'iOS/10.3'}
]
)

def test_load_csv_file_multiple_parameters(self):
csv_file_path = os.path.join(
os.getcwd(), 'tests/data/username-password.csv')
csv_content = testcase.load_file(csv_file_path)
self.assertEqual(
csv_content,
[
{'username': 'test1', 'password': '111111'},
{'username': 'test2', 'password': '222222'},
{'username': 'test3', 'password': '333333'}
]
)

def test_cartesian_product_one(self):
parameters_content_list = [
[
{"a": 1},
{"a": 2}
]
]
product_list = testcase.gen_cartesian_product(*parameters_content_list)
self.assertEqual(
product_list,
[
{"a": 1},
{"a": 2}
]
)

def test_cartesian_product_multiple(self):
parameters_content_list = [
[
{"a": 1},
{"a": 2}
],
[
{"x": 111, "y": 112},
{"x": 121, "y": 122}
]
]
product_list = testcase.gen_cartesian_product(*parameters_content_list)
self.assertEqual(
product_list,
[
{'a': 1, 'x': 111, 'y': 112},
{'a': 1, 'x': 121, 'y': 122},
{'a': 2, 'x': 111, 'y': 112},
{'a': 2, 'x': 121, 'y': 122}
]
)

def test_cartesian_product_empty(self):
parameters_content_list = []
product_list = testcase.gen_cartesian_product(*parameters_content_list)
self.assertEqual(product_list, [])

def test_gen_cartesian_product_parameters_one_to_one(self):
parameters = [
{"user_agent": "random"},
{"app_version": "sequential"}
]
testset_path = os.path.join(
os.getcwd(),
"tests/data/demo_parameters.yml"
)
cartesian_product_parameters = testcase.gen_cartesian_product_parameters(
parameters,
testset_path
)
self.assertEqual(
len(cartesian_product_parameters),
6
)

def test_gen_cartesian_product_parameters_one_to_multiple(self):
parameters = [
{"user_agent": "random"},
{"username-password": "sequential"}
]
testset_path = os.path.join(
os.getcwd(),
"tests/data/demo_parameters.yml"
)
cartesian_product_parameters = testcase.gen_cartesian_product_parameters(
parameters,
testset_path
)
self.assertEqual(
len(cartesian_product_parameters),
9
)

def test_load_yaml_file_file_format_error(self):
yaml_tmp_file = "tests/data/tmp.yml"
# create empty yaml file
Expand Down

1 comment on commit f8569aa

@debugtalk
Copy link
Member

@debugtalk debugtalk commented on f8569aa Feb 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.