Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@named_data decorator to give tests with @data custom names #103

Merged
merged 2 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions named_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import ddt


class NamedDataList(list):
""" This is a helper class for @named_data that allows ddt tests to have meaningful names. """
def __init__(self, name, *args):
super(NamedDataList, self).__init__(args)
self.name = name

def __str__(self):
return str(self.name)


class NamedDataDict(dict):
""" This is a helper class for @named_data that allows ddt tests to have meaningful names. """
def __init__(self, name, **kwargs):
super(NamedDataDict, self).__init__(kwargs)
self.name = name

def __str__(self):
return str(self.name)


# In order to ensure that the name is properly interpreted regardless of arguments, NamedDataXX types must be added to
# the tuple of ddt trivial types for which it always gets the name. See ddt.trivial_types for more information.
ddt.trivial_types += (NamedDataList, NamedDataDict, )


def named_data(*named_values):
"""
This decorator is to allow for meaningful names to be given to tests that would otherwise use @ddt.data and
@ddt.unpack.

Example of original ddt usage:
@ddt.ddt
class TestExample(TemplateTest):
@ddt.data(
[0, 1],
[10, 11]
)
@ddt.unpack
def test_values(self, value1, value2):
...

Example of new usage:
@ddt.ddt
class TestExample(TemplateTest):
@named_data(
['A', 0, 1],
['B', 10, 11],
)
def test_values(self, value1, value2):
...

Note that @unpack is not used.

:param list[Any] | dict[Any,Any] named_values: Each named_value should be a list with the name as the first
argument, or a dictionary with 'name' as one of the keys. The name will be coerced to a string and all other
values will be passed unchanged to the test.
"""
type_of_first = None
values = []
for named_value in named_values:
if type_of_first is None:
type_of_first = type(named_value)

if not isinstance(named_value, type_of_first):
raise TypeError("@named_data expects all values to be of the same type.")

if isinstance(named_value, list):
value = NamedDataList(named_value[0], *named_value[1:])
type_of_first = type_of_first or list

elif isinstance(named_value, dict):
if "name" not in named_value.keys():
raise ValueError("@named_data expects a dictionary with a 'name' key.")
value = NamedDataDict(**named_value)
type_of_first = type_of_first or dict
else:
raise TypeError("@named_data expects a list or dictionary.")

# Remove the __doc__ attribute so @ddt.data doesn't add the NamedData class docstrings to the test name.
value.__doc__ = None

values.append(value)

def wrapper(func):
ddt.data(*values)(ddt.unpack(func))
return func

return wrapper
50 changes: 50 additions & 0 deletions test/test_named_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import ddt
import unittest

from named_data import named_data


@ddt.ddt
class TestNamedData(unittest.TestCase):
class NonTrivialClass(object):
pass

@named_data(
['Single', 0, 1]
)
def test_single_named_value(self, value1, value2):
self.assertGreater(value2, value1)

@named_data(
['1st', 1, 2],
['2nd', 3, 4]
)
def test_multiple_named_value_lists(self, value1, value2):
self.assertGreater(value2, value1)

@named_data(
dict(name='1st', value2=1, value1=0),
{'name': '2nd', 'value2': 1, 'value1': 0}
)
def test_multiple_named_value_dicts(self, value1, value2):
self.assertGreater(value2, value1)

@named_data(
['Passes', NonTrivialClass(), True],
['Fails', 1, False]
)
def test_list_with_nontrivial_type(self, value, passes):
if passes:
self.assertIsInstance(value, self.NonTrivialClass)
else:
self.assertNotIsInstance(value, self.NonTrivialClass)

@named_data(
{'name': 'Passes', 'value': NonTrivialClass(), 'passes': True},
{'name': 'Fails', 'value': 1, 'passes': False}
)
def test_dict_with_nontrivial_type(self, value, passes):
if passes:
self.assertIsInstance(value, self.NonTrivialClass)
else:
self.assertNotIsInstance(value, self.NonTrivialClass)