refactor(test): Update test parameterization

ref: #733 #730 #729
This commit is contained in:
2025-05-01 01:16:57 +09:30
parent 927776b9a7
commit 64757826da
5 changed files with 576 additions and 121 deletions

View File

@ -16,24 +16,48 @@ from app.tests.common import DoesNotExist
class APIFieldsTestCases:
api_fields_common = {
'id': int,
'display_name': str,
'_urls': dict,
'_urls._self': str,
'_urls.notes': str
'id': {
'expected': int
},
'display_name': {
'expected': str
},
'_urls': {
'expected': dict
},
'_urls._self': {
'expected': str
},
'_urls.notes': {
'expected': str
},
}
api_fields_model = {
'model_notes': str,
'created': str,
'modified': str
'model_notes': {
'expected': str
},
'created': {
'expected': str
},
'modified': {
'expected': str
},
}
api_fields_tenancy = {
'organization': dict,
'organization.id': int,
'organization.display_name': str,
'organization.url': Hyperlink,
'organization': {
'expected': dict
},
'organization.id': {
'expected': int
},
'organization.display_name': {
'expected': str
},
'organization.url': {
'expected': Hyperlink
},
}
parametrized_test_data = {
@ -156,12 +180,15 @@ class APIFieldsTestCases:
pass
def test_api_field_exists(self, recursearray, test_name, test_value, expected):
def test_api_field_exists(self, recursearray, parameterized, param_key_test_data,
param_value,
param_expected
):
"""Test for existance of API Field"""
api_data = recursearray(self.api_data, test_value)
api_data = recursearray(self.api_data, param_value)
if expected is DoesNotExist:
if param_expected is DoesNotExist:
assert api_data['key'] not in api_data['obj']
@ -171,18 +198,21 @@ class APIFieldsTestCases:
def test_api_field_type(self, recursearray, test_name, test_value, expected):
def test_api_field_type(self, recursearray, parameterized, param_key_test_data,
param_value,
param_expected
):
"""Test for type for API Field"""
api_data = recursearray(self.api_data, test_value)
api_data = recursearray(self.api_data, param_value)
if expected is DoesNotExist:
if param_expected is DoesNotExist:
assert api_data['key'] not in api_data['obj']
else:
assert type( api_data['value'] ) is expected
assert type( api_data['value'] ) is param_expected

View File

@ -41,11 +41,48 @@ def enable_db_access_for_all_tests(db): # pylint: disable=W0613:unused-argume
def pytest_generate_tests(metafunc):
# test_no_value = {"test_name", "test_value", "expected"} <= set(metafunc.fixturenames)
arg_values:list = None
# test_value = {"test_name", "test_value", "return_value", "expected"} <= set(metafunc.fixturenames)
fixture_parameters: list = []
parameterized_test = False
parameterized_key: str = None
if {'parameterized'} <= set(metafunc.fixturenames):
all_fixture_parameters = metafunc.fixturenames
fixture_parameters += ['parameterized']
for i in range(0, len(metafunc.fixturenames)):
if (
str(all_fixture_parameters[i]).startswith('param_')
and not str(all_fixture_parameters[i]).startswith('param_key_')
):
fixture_parameters += [ all_fixture_parameters[i] ]
elif str(all_fixture_parameters[i]).startswith('param_key_'):
parameterized_key = str( all_fixture_parameters[i] ).replace('param_key_', '')
if len(fixture_parameters) == 1:
fixture_parameters += [ all_fixture_parameters[i] ]
else:
fixture_parameters[1] = all_fixture_parameters[i]
parameterized_test = len(fixture_parameters) > 0
if parameterized_test:
if {"test_name", "test_value", "expected"} <= set(metafunc.fixturenames):
values = {}
@ -55,26 +92,129 @@ def pytest_generate_tests(metafunc):
for base in reversed(cls.__mro__):
base_values = getattr(base, "parametrized_test_data", [])
base_values = getattr(base, 'parametrized_' + parameterized_key, None)
if isinstance(base_values, dict):
if not isinstance(base_values, dict):
continue
if len(values) == 0 and len(base_values) > 0:
values.update(base_values)
continue
for key, value in values.items():
if(
type(value) is not dict
or key not in base_values
):
continue
if key not in values:
values.update({
key: base_values[key]
})
else:
values[key].update( base_values[key] )
for key, value in base_values.items():
if key not in values:
values.update({
key: base_values[key]
})
if values:
metafunc.parametrize(
argnames = (
"test_name", "test_value", "expected"
),
argvalues = [
(field, field, expected) for field, expected in values.items()
],
ids = [
str( field.replace('.', '_') + '_' + getattr(expected, '__name__', 'None').lower() ) for field, expected in values.items()
],
)
ids = []
arg_values:list = []
for item in values.items():
ids_name = item[0]
item_values:tuple = ()
length = len(item)
is_key_value: bool = True
if type(item[1]) is not dict:
continue
item_values += ( None, None, item[0])
for key in fixture_parameters:
if key in [ fixture_parameters[0], fixture_parameters[1], fixture_parameters[2], ]:
# these values are already defined in `item_values`
# fixture_parameters[0] = parameterized.
# fixture_parameters[1] = param_key
# fixture_parameters[2] = the dict name
continue
if(
str(key).startswith('param_')
and not str(key).startswith('param_key_')
):
key = str(key).replace('param_', '')
if (
type(item[1]) is not dict
or item[1].get(key, 'key-does_not-exist') == 'key-does_not-exist'
):
item_values = ()
continue
if key in item[1]:
item_values += ( item[1][key], )
if type(item[1][key]) is type:
ids_name += '_' + getattr(item[1][key], '__name__', 'None').lower()
else:
ids_name += '_' + str(item[1][key]).lower()
if(
len(item_values) > 0
and len(fixture_parameters) == len(item_values)
):
arg_values += [ item_values ]
ids += [ ids_name, ]
if len(arg_values) > 0:
metafunc.parametrize(
argnames = [
*fixture_parameters
],
argvalues = arg_values,
ids = ids,
)

View File

@ -114,93 +114,259 @@ class APITestCases(
parametrized_test_data = {
'model_notes': DoesNotExist,
'_urls.notes': DoesNotExist,
'external_system': int,
'external_ref': int,
'parent_ticket': dict,
'parent_ticket.id': int,
'parent_ticket.display_name': str,
'parent_ticket.url': str,
'ticket_type': str,
'status': int,
'status_badge': dict,
'status_badge.icon': dict,
'status_badge.icon.name': str,
'status_badge.icon.style': str,
'status_badge.text': str,
'status_badge.text_style': str,
'status_badge.url': type(None),
'category': dict,
'category.id': int,
'category.display_name': str,
'category.url': Hyperlink,
'title': str,
'description': str,
'ticket_duration': int,
'ticket_estimation': int,
'project': dict,
'project.id': int,
'project.display_name': str,
'project.url': Hyperlink,
'milestone': dict,
'milestone.id': int,
'milestone.display_name': str,
'milestone.url': str,
'urgency': int,
'urgency_badge': dict,
'urgency_badge.icon': dict,
'urgency_badge.icon.name': str,
'urgency_badge.icon.style': str,
'urgency_badge.text': str,
'urgency_badge.text_style': str,
'urgency_badge.url': type(None),
'impact': int,
'impact_badge': dict,
'impact_badge.icon': dict,
'impact_badge.icon.name': str,
'impact_badge.icon.style': str,
'impact_badge.text': str,
'impact_badge.text_style': str,
'impact_badge.url': type(None),
'priority': int,
'priority_badge': dict,
'priority_badge.icon': dict,
'priority_badge.icon.name': str,
'priority_badge.icon.style': str,
'priority_badge.text': str,
'priority_badge.text_style': str,
'priority_badge.url': type(None),
'opened_by': dict,
'opened_by.id': int,
'opened_by.display_name': str,
'opened_by.first_name': str,
'opened_by.last_name': str,
'opened_by.username': str,
'opened_by.username': str,
'opened_by.is_active': bool,
'opened_by.url': Hyperlink,
'model_notes': {
'expected': DoesNotExist
},
'_urls.notes': {
'expected': DoesNotExist
},
'external_system': {
'expected': int
},
'external_ref': {
'expected': int
},
'parent_ticket': {
'expected': dict
},
'parent_ticket.id': {
'expected': int
},
'parent_ticket.display_name': {
'expected': str
},
'parent_ticket.url': {
'expected': str
},
'ticket_type': {
'expected': str
},
'status': {
'expected': int
},
'status_badge': {
'expected': dict
},
'status_badge.icon': {
'expected': dict
},
'status_badge.icon.name': {
'expected': str
},
'status_badge.icon.style': {
'expected': str
},
'status_badge.text': {
'expected': str
},
'status_badge.text_style': {
'expected': str
},
'status_badge.url': {
'expected': type(None)
},
'category': {
'expected': dict
},
'category.id': {
'expected': int
},
'category.display_name': {
'expected': str
},
'category.url': {
'expected': Hyperlink
},
'title': {
'expected': str
},
'description': {
'expected': str
},
'ticket_duration': {
'expected': int
},
'ticket_estimation': {
'expected': int
},
'project': {
'expected': dict
},
'project.id': {
'expected': int
},
'project.display_name': {
'expected': str
},
'project.url': {
'expected': Hyperlink
},
'milestone': {
'expected': dict
},
'milestone.id': {
'expected': int
},
'milestone.display_name': {
'expected': str
},
'milestone.url': {
'expected': str
},
'urgency': {
'expected': int
},
'urgency_badge': {
'expected': dict
},
'urgency_badge.icon': {
'expected': dict
},
'urgency_badge.icon.name': {
'expected': str
},
'urgency_badge.icon.style': {
'expected': str
},
'urgency_badge.text': {
'expected': str
},
'urgency_badge.text_style': {
'expected': str
},
'urgency_badge.url': {
'expected': type(None)
},
'impact': {
'expected': int
},
'impact_badge': {
'expected': dict
},
'impact_badge.icon': {
'expected': dict
},
'impact_badge.icon.name': {
'expected': str
},
'impact_badge.icon.style': {
'expected': str
},
'impact_badge.text': {
'expected': str
},
'impact_badge.text_style': {
'expected': str
},
'impact_badge.url': {
'expected': type(None)
},
'priority': {
'expected': int
},
'priority_badge': {
'expected': dict
},
'priority_badge.icon': {
'expected': dict
},
'priority_badge.icon.name': {
'expected': str
},
'priority_badge.icon.style': {
'expected': str
},
'priority_badge.text': {
'expected': str
},
'priority_badge.text_style': {
'expected': str
},
'priority_badge.url': {
'expected': type(None)
},
'opened_by': {
'expected': dict
},
'opened_by.id': {
'expected': int
},
'opened_by.display_name': {
'expected': str
},
'opened_by.first_name': {
'expected': str
},
'opened_by.last_name': {
'expected': str
},
'opened_by.username': {
'expected': str
},
'opened_by.username': {
'expected': str
},
'opened_by.is_active': {
'expected': bool
},
'opened_by.url': {
'expected': Hyperlink
},
'subscribed_to': list,
'subscribed_to.0.id': int,
'subscribed_to.0.display_name': str,
'subscribed_to.0.url': str,
'subscribed_to': {
'expected': list
},
'subscribed_to.0.id': {
'expected': int
},
'subscribed_to.0.display_name': {
'expected': str
},
'subscribed_to.0.url': {
'expected': str
},
'assigned_to': list,
'assigned_to.0.id': int,
'assigned_to.0.display_name': str,
'assigned_to.0.url': str,
'assigned_to': {
'expected': list
},
'assigned_to.0.id': {
'expected': int
},
'assigned_to.0.display_name': {
'expected': str
},
'assigned_to.0.url': {
'expected': str
},
'planned_start_date': str,
'planned_finish_date': str,
'real_start_date': str,
'real_finish_date': str,
'planned_start_date': {
'expected': str
},
'planned_finish_date': {
'expected': str
},
'real_start_date': {
'expected': str
},
'real_finish_date': {
'expected': str
},
'is_deleted': bool,
'is_solved': bool,
'date_solved': str,
'is_closed': bool,
'date_closed': str,
'is_deleted': {
'expected': bool
},
'is_solved': {
'expected': bool
},
'date_solved': {
'expected': str
},
'is_closed': {
'expected': bool
},
'date_closed': {
'expected': str
},
}

View File

@ -9,9 +9,12 @@ class TicketSLMAPITestCases(
):
parametrized_test_data = {
'tto': int,
'ttr': int,
'tto': {
'expected': int
},
'ttr': {
'expected': int
}
}
kwargs_create_item: dict = {

View File

@ -151,6 +151,122 @@ Due to how pytest and pytest-django works, there is no method available for clas
<!-- markdownlint-restore -->
## Parameterizing Tests
To be able to paramertize any test case, the test must be setup to use PyTest. Within the test class the test data is required to be stored in a dictionary prefixed with string `paramaterized_<data_name>`. Variable `<data_name>` is the data key that you will specify within the test method.
Our test setup allows for class inheritance which means you can within each class of the inheritance chain, add the `paramaterized_<data_name>` attribute. If you do this, starting from the lowest base class, each class that specifies the `paramaterized_<data_name>` attribute will be merged. The merge is an overwrite of the classes previous base class, meaning that the classes higher in the chain will overwrite the value of the lower class in the inheritance chain. You can not however remove a key from attribute `paramaterized_<data_name>`.
The test method must be called with parameters:
- 'parameterized'
Tells the test setup that this test case is a parameterized test.
- `param_key_<data_name>`
Tells the test setup the suffix to use to find the test data. The value of variable `data_name` can be any value you wish as long as it only contains chars `a-z` and/or `_` (underscore). This value is also used in class parameter `paramaterized_<data_name>`.
- `param_<name>`
Tells the test setup that this is data to be passed from the test. When test setup is run, these attributes will contain the test data. It's of paramount importance, that the dict You can have as many of these attributes you wish, as long as `<name>` is unique and `<name>` is always prefixed with `param_`. If you specify more than to parameters with the `param_` prefix, the value after the `param_` prefix, must match the dictionary key for the data you wish to be assigned to that parameter. what ever name you give the first `param_` key, will always receive the key name from the `parameterized_test_data` attribute in the test class.
The value of `<name>` for each and in the order specified is suffixed to the test case name
``` py
class MyTestClassTestCases:
parameterized_test_data: dict = {
'key_1': {
'expected': 'key_1'
},
'key_2': {
'random': 'key_2'
},
}
class MyTestClassPyTest(
MyTestClassTestCases
):
parameterized_test_data: dict = {
'key_2': {
'random': 'value'
}
'key_3': {
'expected': 'key_3',
'is_type': bool
}
}
parameterized_second_dict: dict = {
'key_1': {
'expected': 'key_1'
},
}
def test_my_test_case_one(self, parameterized, param_key_test_data, param_value, param_expected):
assert param_value == param_expected
def test_my_test_case_two(self, parameterized, param_key_test_data, param_value, param_random):
assert param_value == param_random
def test_my_test_case_three(self, parameterized, param_key_test_data, param_value, param_is_type):
my_test_dict = self.adict
assert type(my_test_dict[param_value]) is param_is_type
def test_my_test_case_four(self, parameterized, param_key_second_dict, param_arbitrary_name, param_expected):
my_test_dict = self.a_dict_that_is_defined_in_the_test_class
assert my_test_dict[param_arbitrary_name] == param_expected
```
In this example:
- The test class in this case is `MyTestClassPyTest` which inherits from `MyTestClassTestCases`. there are two parameterized variables: `test_data` and `second_dict`. Although, the concrete class attribute `parameterized_test_data` overrides the base classes variable of the same name, the test setup logic does merge `MyTestClassPyTest.parameterized_test_data` with `MyTestClassTestCases.parameterized_test_data`. So in this case the value dictionary `MyTestClassPyTest.parameterized_test_data[key_2][random]`, `value` will overwrite dictionary of the same name in the base class. In the same token, as dictionary `MyTestClassTestCases.parameterized_test_data[key_3]` does not exist, it will be added to the dictionary during merge so it exists in `MyTestClassPyTest.parameterized_test_data`
- test suite `MyTestClassPyTest` will create a total of five parmeterized test cases for the following reasons:
- `test_my_test_case_one` will create two parameterized test cases.
- will use data in attribute `test_data` prefixed with `parameterized_` as this is the attribute prefixed with `param_key_`.
- `MyTestClassPyTest.parameterized_test_data['key_1']` is a dictionary, which contains key `expected` which is also one of the attributes specified with prefix `param_`
- `MyTestClassPyTest.parameterized_test_data['key_3']` is a dictionary, which contains key `expected` which is also one of the attributes specified with prefix `param_`
- `test_my_test_case_two` will create one parameterized test case.
- will use data in attribute `test_data` prefixed with `parameterized_` as this is the attribute prefixed with `param_key_`.
- `MyTestClassPyTest.parameterized_test_data['key_2']` is a dictionary, which contains key `random` which is also one of the attributes specified with prefix `param_`
- `test_my_test_case_three` will create one parameterized test case.
- will use data in attribute `test_data` prefixed with `parameterized_` as this is the attribute prefixed with `param_key_`.
- `MyTestClassPyTest.parameterized_test_data['key_3']` is a dictionary, which contains key `is_type` which is also one of the attributes specified with prefix `param_`
- `test_my_test_case_four` will create one parameterized test case.
- will use data in attribute `second_dict` prefixed with `parameterized_` as this is the attribute prefixed with `param_key_`.
- `MyTestClassPyTest.parameterized_second_dict['key_1']` is a dictionary, which contains key `expected` which is also one of the attributes specified with prefix `param_`
## Running Tests
Test can be run by running the following: