diff --git a/ddt.py b/ddt.py index a084695..f941644 100644 --- a/ddt.py +++ b/ddt.py @@ -6,6 +6,8 @@ # https://github.com/txels/ddt/blob/master/LICENSE.md import inspect +import io +import itertools import json import os import re @@ -18,59 +20,420 @@ # They are added to the decorated test method and processed later # by the `ddt` class decorator. -DATA_ATTR = '%values' # store the data the test must run with -FILE_ATTR = '%file_path' # store the path to JSON file -UNPACK_ATTR = '%unpack' # remember that we have to unpack values +PARAMS_SETS_ATTR = '%params_sets' # store a list of *ParamsSet objects +UNPACKALL_ATTR = '%unpackall' # remember @unpackall decorators + + +# Types that can be converted into valid identifiers safely. +TRIVIAL_TYPES = (type(None), bool, str, int, float) + +# Extend the list a bit for Python2 +try: + TRIVIAL_TYPES += (unicode,) +except NameError: + pass + + +# Public interface - Decorators def unpack(func): """ - Method decorator to add unpack feature. + Method decorator to unpack parameters from the next (syntactically) + parameters set (``@data`` or ``@file_data`` decorator) by one level. + + Multiple levels are unpacked if ``@unpack`` and ``@unpackall`` are combined + and/or applied multiple times. + + """ + getattr(func, PARAMS_SETS_ATTR)[0].unpack() + return func + + +def unpackall(func): + """ + Method decorator to unpack parameters in all parameter sets by one level. + + Multiple levels are unpacked if ``@unpack`` and ``@unpackall`` are combined + and/or applied multiple times. """ - setattr(func, UNPACK_ATTR, True) + setattr(func, UNPACKALL_ATTR, getattr(func, UNPACKALL_ATTR, 0) + 1) return func -def data(*values): +def data(*unnamed_values, **named_values): """ - Method decorator to add to your test methods. + Method decorator to supply parameters to your test methods in-line. Should be added to methods of instances of ``unittest.TestCase``. + Keyword arguments can be used to explicitly define names of values to be + used in names of created test methods. + """ def wrapper(func): - setattr(func, DATA_ATTR, values) + if not hasattr(func, PARAMS_SETS_ATTR): + setattr(func, PARAMS_SETS_ATTR, []) + params = InlineDataValues(*unnamed_values, **named_values) + getattr(func, PARAMS_SETS_ATTR).insert(0, params) return func return wrapper -def file_data(value): +def file_data(filepath, encoding=None): """ - Method decorator to add to your test methods. + Method decorator to supply parameters to your test method from a JSON file. Should be added to methods of instances of ``unittest.TestCase``. - ``value`` should be a path relative to the directory of the file - containing the decorated ``unittest.TestCase``. The file - should contain JSON encoded data, that can either be a list or a - dict. + ``filepath`` should be a path relative to the directory of the file + containing the decorated ``unittest.TestCase``. - In case of a list, each value in the list will correspond to one - test case, and the value will be concatenated to the test method - name. - - In case of a dict, keys will be used as suffixes to the name of the - test case, and values will be fed as test data. + The file should contain JSON-encoded data, that can either be a list or a + dict. A list supplies unnamed parameters. A dict supplies named parameters + where keys are used to identify individual parameters. """ def wrapper(func): - setattr(func, FILE_ATTR, value) + if not hasattr(func, PARAMS_SETS_ATTR): + setattr(func, PARAMS_SETS_ATTR, []) + params = FileDataValues(filepath, encoding=encoding) + getattr(func, PARAMS_SETS_ATTR).insert(0, params) return func return wrapper +def ddt(cls): + """ + Class decorator for subclasses of ``unittest.TestCase``. + + Apply this decorator to the test case class, and then decorate test methods + with ``@data``, ``@file_data``, ``@unpack``, and ``@unpackall``. + + For each method decorated with ``@data`` and ``@file_data``, this will + effectively create one method for each member of the Cartesian product of + data items passed to ``@data`` or read from a JSON file by ``@file_data``. + + The names of the test methods follow the pattern + ``original_test_name(__{ordinal}_{data})+``. + + The part ``__{ordinal}_{data}`` is repeated as many times as there are + nested ``@data`` and ``@file_data`` decorators. + + ``ordinal`` is the position of a particular data item in the list of + options represented by the corresponding decorator. Positions are numbered + from 0. Keyword arguments (for ``@data``) and members of top-level dict + (for ``@data_file``) are ordered according to their keys. + + ``data`` attempts to provide human readable identification of the data item + with the following priority: 1) The keyword is used for data items passed + as keyword arguments (for ``@data``) or members of the top-level dict (for + ``@file_data``). 2) The ``__name__`` attribute of the value if it exists. + 3) An explicit namestring representation of the data value converted into a + valid python identifier, if possible. 4) Only the position is used. + + For each method decorated with ``@file_data('test_data.json')``, the + decorator will try to load the test_data.json file located relative + to the python file containing the method that is decorated. It will, + for each ``test_name`` key create as many methods in the list of values + from the ``data`` key. + + """ + for name, func in list(cls.__dict__.items()): + if hasattr(func, PARAMS_SETS_ATTR): + test_vectors = itertools.product( + *[s.use_class(cls) for s in getattr(func, PARAMS_SETS_ATTR)] + ) + for vector in test_vectors: + params = sum(vector, Params(name, [], {})) + params.unpack(getattr(func, UNPACKALL_ATTR, 0)) + add_test(cls, func, params) + delattr(cls, name) + + return cls + + +# Adding new tests + + +def add_test(cls, func, params): + """Add a test derived from an original function and specific combination of + values provided by ``@data`` and ``@file_data`` decorators. + + """ + if isinstance(params, Params): + @wraps(func) + def test_case_func(self): + return func(self, *params.args, **params.kwargs) + + else: + def test_case_func(self): # pylint: disable=unused-argument + raise params.reason + + test_case_func.__name__ = params.name + setattr(cls, params.name, test_case_func) + + +# Internal data structures + + +class ParamsFailure(object): + """ + FileDataValues generates an instance of this class instead of instances of + Params in case it cannot load the data file. A fake test method is + generated instead of a regular one if it has an instance of this class + among its parameters. + + ParamsFailure supports the same interface as Params. + + More formally, instances of Params and ParamsFailure form a semigroup with + respect to addition (+) with ``Params(None, [], {}`` being a left identity. + The ``unpack()`` method is a homomorphism. + + Instances of ParamsFailure act almost as absorbing elements (0.X=0, X.0=0). + The left-most instance of ParamsFailure in a sum prevails, only the name + keeps updating. + + """ + + def __init__(self, name, reason): + self.name = name + self.reason = reason + + def unpack(self, N=1): # pylint: disable=unused-argument + """ + Do nothing but return self (convenient for use in mappings). + + """ + return self + + def __add__(self, other): + """ + Return a sum of self and an instance of Param or ParamFailure. + + The sum is a new instance of ParamFailure with the `reason` from the + left operand and a name combining names of both operands. + + """ + new_name = combine_names(self.name, other.name) + return ParamsFailure(new_name, self.reason) + + +class Params(object): + """ + Instances of Params form a semigroup with respect to addition. + ``Params(None, [], {})`` constitutes the identity. The ``unpack()`` + function is a homomorphism. + + """ + + def __init__(self, name, args, kwargs): + self.name = name + self.args = args + self.kwargs = kwargs + + def unpack(self, count=1): + """ + Recursively unpack positional arguments `count`-times. + + """ + while count > 0: + self.unpack_one_level() + count = count - 1 + return self + + def unpack_one_level(self): + """ + Unpack one level of positional arguments. + + Members of lists and tuples replace its parents as new positional + arguments. Members of dicts are used to update keyword arguments. Other + positional arguments are left intact. + + """ + new_args = [] + + for value in self.args: + if isinstance(value, list) or isinstance(value, tuple): + new_args.extend(value) + elif isinstance(value, dict): + self.kwargs.update(value) + else: + new_args.append(value) + + self.args = new_args + + def __add__(self, other): + """ + Return a sum of self and an instance of Param or ParamFailure. + + If combined with Params, positional arguments in the right operand are + concatenated to positional arguments in the left operand and keywrd + arguments in the right operand are used to update keyword arguments in + the left operand. + + Note: Addition modifies the left-most instance of Params in a sum. This + is OK for the expected use where a fresh identity Params(None, [], {}) + is supplied as the leftmost operand. + + If combined with ParamsFailure, a new instance of ParamsFailure is + returned. + """ + new_name = combine_names(self.name, other.name) + + if isinstance(other, ParamsFailure): + return ParamsFailure(new_name, other.reason) + + self.name = new_name + self.args.extend(other.args) + self.kwargs.update(other.kwargs) + return self + + +class InlineDataValues(object): + """ + This class represents values supplied to a ``data`` decorator. + + Instances of this class are generators which yield an instance of Params + for each positional and keyword argument passed to the constructor. + + """ + + def __init__(self, *unnamed_values, **named_values): + """ + Stores all arguments passed to the constructor for later use. + + """ + self.unpack_count = 0 + self.unnamed_values = unnamed_values + self.named_values = named_values + + def unpack(self): + """ + Increases by one the number of times values should be unpacked before + passing them to the test function. + + This method is called directly by the ``unpack`` decorator. + + """ + self.unpack_count = self.unpack_count + 1 + + def use_class(self, cls): # pylint: disable=unused-argument + """ + Does nothing and returns self. + + This method just makes sure that `InlineDataValues` and + `FileDataValues` implement the same interface. + + """ + return self + + def __iter__(self): + for idx, value in enumerate(self.unnamed_values): + name = make_params_name(idx, None, value) + yield Params(name, [value], {}).unpack(self.unpack_count) + + for idx, key in enumerate( + sorted(self.named_values), + start=len(self.unnamed_values) + ): + value = self.named_values[key] + name = make_params_name(idx, key, value) + yield Params(name, [value], {}).unpack(self.unpack_count) + + +class FileDataValues(object): + """This class represents values supplied to a ``file_data`` decorator. + + Instances of this class are generators which load data from a given JSON + file and yield an instance of Params for each member of the top-level + structure (``list`` or ``dict``). + + The generator yields just one item if it fails to load data from the file. + The item is an instance of ParamsFailure that carries details about the + failure. + + """ + + def __init__(self, filepath, encoding=None): + """ + Stores all arguments passed to the constructor for later use. + + """ + self.unpack_count = 0 + self.pathbase = '' + self.filepath = filepath + self.encoding = encoding + + def use_class(self, cls): + """ + Sets base path for reading the file to the directory that contains the + module where the class ``cls`` is defined. + + """ + + # Is there perhaps a way the constructor could retrieve this + # information somehow? This smells like a workaround that messes the + # interface. + + cls_path = os.path.abspath(inspect.getsourcefile(cls)) + self.pathbase = os.path.dirname(cls_path) + return self + + def unpack(self): + """ + Increase by one the number of times values should be unpacked before + passing them to the test function. + + This method is called directly by the @unpack decorator. + + """ + self.unpack_count = self.unpack_count + 1 + + def load_values(self): # pylint: disable=unused-argument + try: + filepath = os.path.join(self.pathbase, self.filepath) + with io.open(filepath, encoding=self.encoding) as f: + values = json.load(f) + + return values + + except IOError as reason: + # IOError is an alias for OSError since Python 3.3 + return reason + + except ValueError as reason: + return reason + + def __iter__(self): + values = self.load_values() + + if isinstance(values, Exception): + yield ParamsFailure(values.__class__.__name__, values) + + elif isinstance(values, list): + for idx, value in enumerate(values): + name = make_params_name(idx, None, value) + yield Params(name, [value], {}).unpack(self.unpack_count) + + elif isinstance(values, dict): + for idx, key in enumerate(sorted(values)): + value = values[key] + name = make_params_name(idx, key, value) + yield Params(name, [value], {}).unpack(self.unpack_count) + + +# Test name generation + + def is_hash_randomized(): + """ + Check whether hashes are randomized in the current Python version. + + See `convert_to_name` for details. + + """ return (((sys.hexversion >= 0x02070300 and sys.hexversion < 0x03000000) or (sys.hexversion >= 0x03020300)) and @@ -78,19 +441,35 @@ def is_hash_randomized(): 'PYTHONHASHSEED' not in os.environ) -def mk_test_name(name, value, index=0): +def is_trivial(value): """ - Generate a new name for a test case. + Check whether a value is of a trivial type (w.r.t. `TRIVIAL_TYPES`). - It will take the original test name and append an ordinal index and a - string representation of the value, and convert the result into a valid - python identifier by replacing extraneous characters with ``_``. + """ + if isinstance(value, TRIVIAL_TYPES): + return True - If hash randomization is enabled (a feature available since 2.7.3/3.2.3 - and enabled by default since 3.3) and a "non-trivial" value is passed - this will omit the name argument by default. Set `PYTHONHASHSEED` - to a fixed value before running tests in these cases to get the - names back consistently or use the `__name__` attribute on data values. + if isinstance(value, (list, tuple)): + return all([is_trivial(v) for v in value]) + + return False + + +def convert_to_name(value): + """ + Convert a value into a string that can safely be used in an attribute name. + + Returns a string representation of the value with all extraneous characters + replaced with ``_``. Sequences of underscores are reduced to one, leading + and trailing underscores are trimmed. + + Raises ValueError if it cannot convert the value (see the note below). + + Note: If hash randomization is enabled (a feature available since + 2.7.3/3.2.3 and enabled by default since 3.3) and a "non-trivial" value is + passed this will omit the name argument by default. Set `PYTHONHASHSEED` to + a fixed value before running tests in these cases to get the names back + consistently or use the `__name__` attribute on data values. A "trivial" value is a plain scalar, or a tuple or list consisting only of trivial values. @@ -109,121 +488,52 @@ def mk_test_name(name, value, index=0): # # Trivial scalar values are passed as is in all cases. - trivial_types = (type(None), bool, str, int, float) - try: - trivial_types += (unicode,) - except NameError: - pass - - def is_trivial(value): - if isinstance(value, trivial_types): - return True - - if isinstance(value, (list, tuple)): - return all(map(is_trivial, value)) - - return False - - if is_hash_randomized() and not is_trivial(value): - return "{0}_{1}".format(name, index + 1) + if not is_trivial(value) and is_hash_randomized(): + raise ValueError("Cannot convert complex type: {0}", str(type(value))) try: value = str(value) except UnicodeEncodeError: # fallback for python2 value = value.encode('ascii', 'backslashreplace') - test_name = "{0}_{1}_{2}".format(name, index + 1, value) - return re.sub('\W|^(?=\d)', '_', test_name) - - -def feed_data(func, new_name, *args, **kwargs): - """ - This internal method decorator feeds the test data item to the test. - """ - @wraps(func) - def wrapper(self): - return func(self, *args, **kwargs) - wrapper.__name__ = new_name - return wrapper + value = re.sub('\\W', '_', value) + return re.sub('_+', '_', value).strip('_') -def add_test(cls, test_name, func, *args, **kwargs): +def make_params_name(idx, name, value): """ - Add a test case to this class. + Generate a name for a value in a parameters set. - The test will be based on an existing function but will give it a new - name. + It will take the ordinal index of the value in the set and a name + constructed from one of the follwing items (with decreasing priority): + `name`, `value.__name__`, `value`. See also `convert_to_name`. + If all of that fails, only the ordinal index is used. """ - setattr(cls, test_name, feed_data(func, test_name, *args, **kwargs)) + if name is None: + name = getattr(value, '__name__', value) -def process_file_data(cls, name, func, file_attr): - """ - Process the parameter in the `file_data` decorator. - - """ - cls_path = os.path.abspath(inspect.getsourcefile(cls)) - data_file_path = os.path.join(os.path.dirname(cls_path), file_attr) + try: + name = convert_to_name(name) + return "{0}_{1}".format(idx, name) - def _raise_ve(*args): # pylint: disable-msg=W0613 - raise ValueError("%s does not exist" % file_attr) + except ValueError: + pass - if os.path.exists(data_file_path) is False: - test_name = mk_test_name(name, "error") - add_test(cls, test_name, _raise_ve, None) - else: - data = json.loads(open(data_file_path).read()) - for i, elem in enumerate(data): - if isinstance(data, dict): - key, value = elem, data[elem] - test_name = mk_test_name(name, key, i) - elif isinstance(data, list): - value = elem - test_name = mk_test_name(name, value, i) - add_test(cls, test_name, func, value) + return "{0}".format(idx) -def ddt(cls): +def combine_names(name1, name2): """ - Class decorator for subclasses of ``unittest.TestCase``. - - Apply this decorator to the test case class, and then - decorate test methods with ``@data``. - - For each method decorated with ``@data``, this will effectively create as - many methods as data items are passed as parameters to ``@data``. - - The names of the test methods follow the pattern - ``original_test_name_{ordinal}_{data}``. ``ordinal`` is the position of the - data argument, starting with 1. - - For data we use a string representation of the data value converted into a - valid python identifier. If ``data.__name__`` exists, we use that instead. - - For each method decorated with ``@file_data('test_data.json')``, the - decorator will try to load the test_data.json file located relative - to the python file containing the method that is decorated. It will, - for each ``test_name`` key create as many methods in the list of values - from the ``data`` key. + Combine two names using two underscore characters ``__``. ``None`` can be + used as the identity. """ - for name, func in list(cls.__dict__.items()): - if hasattr(func, DATA_ATTR): - for i, v in enumerate(getattr(func, DATA_ATTR)): - test_name = mk_test_name(name, getattr(v, "__name__", v), i) - if hasattr(func, UNPACK_ATTR): - if isinstance(v, tuple) or isinstance(v, list): - add_test(cls, test_name, func, *v) - else: - # unpack dictionary - add_test(cls, test_name, func, **v) - else: - add_test(cls, test_name, func, v) - delattr(cls, name) - elif hasattr(func, FILE_ATTR): - file_attr = getattr(func, FILE_ATTR) - process_file_data(cls, name, func, file_attr) - delattr(cls, name) - return cls + if name1 is None: + return name2 + elif name2 is None: + return name1 + else: + return"{0}__{1}".format(name1, name2) diff --git a/docs/example.rst b/docs/example.rst deleted file mode 100644 index c7addb4..0000000 --- a/docs/example.rst +++ /dev/null @@ -1,57 +0,0 @@ -Example usage -============= - -DDT consists of a class decorator ``ddt`` (for your ``TestCase`` subclass) -and two method decorators (for your tests that want to be multiplied): - -* ``data``: contains as many arguments as values you want to feed to the test. -* ``file_data``: will load test data from a JSON file. - -Normally each value within ``data`` will be passed as a single argument to -your test method. If these values are e.g. tuples, you will have to unpack them -inside your test. Alternatively, you can use an additional decorator, -``unpack``, that will automatically unpack tuples and lists into multiple -arguments, and dictionaries into multiple keyword arguments. See examples -below. - -This allows you to write your tests as: - -.. literalinclude:: ../test/test_example.py - :language: python - -Where ``test_data_dict.json``: - -.. literalinclude:: ../test/test_data_dict.json - :language: javascript - -and ``test_data_list.json``: - -.. literalinclude:: ../test/test_data_list.json - :language: javascript - -And then run them with your favourite test runner, e.g. if you use nose:: - - $ nosetests -v test/test_example.py - -.. - program-output:: nosetests -v ../test/test_example.py - -The number of test cases actually run and reported separately has been -multiplied. - - -DDT will try to give the new test cases meaningful names by converting the -data values to valid python identifiers. - - -.. note:: - - Python 2.7.3 introduced *hash randomization* which is by default - enabled on Python 3.3 and later. DDT's default mechanism to - generate meaningful test names will **not** use the test data value - as part of the name for complex types if hash randomization is - enabled. - - You can disable hash randomization by setting the - ``PYTHONHASHSEED`` environment variable to a fixed value before - running tests (``export PYTHONHASHSEED=1`` for example). diff --git a/docs/index.rst b/docs/index.rst index 4e340f8..603ec57 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,21 +1,20 @@ Welcome to DDT's documentation! =============================== -DDT (Data-Driven Tests) allows you to multiply one test case -by running it with different test data, and make it appear as -multiple test cases. +DDT (Data-Driven Tests) allows you to multiply one test by running it with +different test data, and make it appear as multiple tests. You can find (and fork) the project in https://github.com/txels/ddt. DDT should work on Python2 and Python3, but we only officially test it for -versions 2.7 and 3.3. +versions 2.7, 3.3, and 3.4. Contents: .. toctree:: :maxdepth: 2 - example + tutorial api Indices and tables diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..6d9e0f2 --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,213 @@ +Tutorial +======== + + +Imagine you have prepared a module that implements a function +``larger_than_two()``. The function takes a number and returns either `True` or +`False` depending on the condition 'larger than two'. You would like to test +the function on multiple values. + + +Tests without DDT +----------------- + +Without DDT, you would perhaps add a test for each value to your test case: + +.. literalinclude:: ../test/test_example01.py + :language: python + +Run with your favorite test runner, it will yield output like this:: + + $ nosetests -v test.test_example01.py + +.. program-output:: nosetests -w .. -v test/test_example01.py + +The test case is verbose and difficult to maintain, especially during early +stages of development. If the function call changes, four tests will have to be +updated manually. + + +Very Basic DDT Usage +-------------------- + +This is where DDT helps. You can use the ``ddt`` and ``data`` decorators to +generate the same tests as above using this code: + +.. literalinclude:: ../test/test_example02.py + :language: python + +Run with your favorite test runner, it will yield output like this:: + + $ nosetests -v test.test_example02.py + +.. program-output:: nosetests -w .. -v test/test_example02.py + +Four tests have been generated from the function ``test_decorated()`` -- one +test for each argument of the ``data`` decorator. Test names were autogenerated +by DDT from the supplied values. + +.. note:: + + Python 2.7.3 introduced *hash randomization* which is by default + enabled on Python 3.3 and later. DDT's default mechanism to + generate meaningful test names will **not** use the test data value + as part of the name for complex types if hash randomization is + enabled. Only the ordinal number would be used. + + You can disable hash randomization by setting the + ``PYTHONHASHSEED`` environment variable to a fixed value before + running tests (``export PYTHONHASHSEED=1`` for example). + + +Name Your Data +-------------- + +While DDT does its best at automatically generating meaningful test names from +supplied values, it cannot beat names wisely chosen by a human. Thus, it gives +you the possibility to name your data: just use keyword arguments instead of +positional ones with the ``data`` decorator. + +.. literalinclude:: ../test/test_example03.py + :language: python + +Run with your favorite test runner, it will yield the same output as if all +four tests were written manually:: + + $ nosetests -v test.test_example03.py + +.. program-output:: nosetests -w .. -v test/test_example03.py + + +Unpack ``list`` Parameters as Positional Arguments +-------------------------------------------------- + +So now we have short code and meaningful test names. However, the code is not +exactly clear as we are getting all test parameters as a single list. This is +where the ``unpack`` decorator comes in handy. + +.. literalinclude:: ../test/test_example04.py + :language: python + +The ``unpack`` decorator applies to the next ``data`` (or ``file_data``) +decorator (next in the source code; they are actually applied in the reverse +order). If an argument of the ``data`` decorator is a list or tuple, the +``unpack`` decorator unpacks the elements into positional arguments of the test +function. + + +Unpack ``dict`` Parameters as Keyword Arguments +----------------------------------------------- + +The ``unpack`` decorator also unpacks ``dict`` values into keyword arguments. +The previous example could be written as follows, with identical results: + +.. literalinclude:: ../test/test_example05.py + :language: python + + +Unpack Twice to Get Positional and Keyword Arguments +---------------------------------------------------- + +The true benefit of unpacking ``dict`` arguments emerges when your test subject +has an optional parameter. Let's say we extend our function ``larger_than_two`` +to accept strings. The function will take an optional argument to specify the +base to be used to convert the string into a number. Base 10 should be used by +default. + +A test for the extended functionality could look like this: + +.. literalinclude:: ../test/test_example06.py + :language: python + +There is no limit on how many times test parameters can be unpacked. Just keep +inserting the ``unpack`` deocrator in front of a ``data`` (or ``file_data``) +decorator. + + +Read Test Parameters from File +------------------------------ + +If a test is to be repeated for many variants of test parameters or if test +parameters contain long or complex values, it is not convenient to specify them +inline. You can read them from a JSON file instead using the ``file_data`` +decorator. + +If the top-level structure in the JSON data is a ``list``, individual elements +are processed as if they were given as positional arguments to the ``data`` +decorator. + +If the top-level structure in the JSON data is a ``dict``, individual elements +are processed as if they were given as keyword arguments to the ``data`` +decorator. + +Thus, with ``json_list.json`` containing + +.. literalinclude:: ../test/json_list.json + :language: javascript + +and ``json_dict.json`` containing + +.. literalinclude:: ../test/json_dict.json + :language: javascript + +the tests ``test_with_json_dict()`` and ``test_with_json_list()`` in the +following test case are equivalent to the test in the previous example. The +only difference is that ``test_with_json_list()`` will generate test names +automatically. + +.. literalinclude:: ../test/test_example07.py + :language: python + +The ``encoding`` argument is optional and defaults to ``None``; the system +default encoding is used in that case. + +Run with your favorite test runner, the test case will yield output like this:: + + $ nosetests -v test.test_example07.py + +.. program-output:: nosetests -w .. -v test/test_example07.py + + +Nest ``data`` and ``file_data`` to Generate Even More Tests +----------------------------------------------------------- + +Imagine that you want to test that a function is a homomorphism, i.e. ``f(a+b) += f(a) + f(b)``. Instead of listing all combinations of ``a`` and ``b`` +explicitly, you can nest ``data`` and ``file_data``. A new test will be defined +for each combination of values from the nested decorators. + +For instance, the following test case will run 20 tests: + +.. literalinclude:: ../test/test_example08.py + :language: python + +Another use case for nested ``data`` and ``file_data`` decorators is testing +default values of optional arguments. Unpacking of parameters can be specified +for each data set independently. + +For instance, the following test cases consists of 12 tests testing the +``str.split()`` method. + +.. literalinclude:: ../test/test_example09.py + :language: python + +.. note:: + + Apart from rare cases like the examples in this tutorial, tests whose + parameters come from two independent sets indicate that your code, be it the + test subject or the test itself, does two unrelated things which should be + decoupled. Use nesting with caution. + + +Save Lines of Code with ``unpackall`` +------------------------------------- + +It is likely that if you want to unpack parameters in one data set, you will +want to unpack parameters from all nested data sets. DDT has the ``unpackall`` +decorator to help you save some lines of code in this case. + +The following code specifies essentially the same tests of ``str.split()`` as the +previous example but in a more succinct way. + +.. literalinclude:: ../test/test_example10.py + :language: python diff --git a/test/json_dict.json b/test/json_dict.json new file mode 100644 index 0000000..cc478cf --- /dev/null +++ b/test/json_dict.json @@ -0,0 +1,7 @@ +{ + "base2_10": [ [ false, "10" ], { "base":2 } ], + "base3_10": [ [ true, "10" ], {"base":3 } ], + "base10_10": [ [ true, "10" ], {"base":10 } ], + "default_10": [ [ true, "10" ], {} ] +} + diff --git a/test/json_list.json b/test/json_list.json new file mode 100644 index 0000000..6d4dc37 --- /dev/null +++ b/test/json_list.json @@ -0,0 +1,6 @@ +[ + [ [ false, "10" ], { "base": 2} ], + [ [ true, "10" ], { "base": 3 } ], + [ [ true, "10" ], { "base": 10 } ], + [ [ true, "10" ], { } ] +] diff --git a/test/mycode.py b/test/mycode.py index bd562c8..a36d641 100644 --- a/test/mycode.py +++ b/test/mycode.py @@ -3,7 +3,11 @@ """ -def larger_than_two(value): +def larger_than_two(value, base=10): + try: + value = int(value, base=base) + except: + pass return value > 2 diff --git a/test/test_data_invalid.json b/test/test_data_invalid.json new file mode 100644 index 0000000..e534a76 --- /dev/null +++ b/test/test_data_invalid.json @@ -0,0 +1,3 @@ +[ + "Hello + diff --git a/test/test_data_utf8sig.json b/test/test_data_utf8sig.json new file mode 100644 index 0000000..cbf6de6 --- /dev/null +++ b/test/test_data_utf8sig.json @@ -0,0 +1,3 @@ +[ + "Ř" +] diff --git a/test/test_ddt2_internals.py b/test/test_ddt2_internals.py new file mode 100644 index 0000000..e2ded88 --- /dev/null +++ b/test/test_ddt2_internals.py @@ -0,0 +1,277 @@ + +from unittest import TestCase + +import six + +import ddt + + +class TestMakeParamsName(TestCase): + + def test__unnamed_int(self): + self.assertEqual(ddt.make_params_name(3, None, 4), '3_4') + + def test__named_int(self): + self.assertEqual(ddt.make_params_name(3, 'myint', 4), '3_myint') + + def test__unnamed_string(self): + self.assertEqual(ddt.make_params_name(3, None, 'a'), '3_a') + + def test__named_string(self): + self.assertEqual( + ddt.make_params_name(3, 'mystring', 'a'), + '3_mystring' + ) + + def test__unnamed_array_of_strings(self): + self.assertEqual(ddt.make_params_name(3, None, ['a', 'b']), '3_a_b') + + def test__named_array_of_strings(self): + self.assertEqual( + ddt.make_params_name(3, 'myarray', ['a', 'b']), + '3_myarray' + ) + + def test__unnamed_complex_type(self): + if ddt.is_hash_randomized(): + self.assertEqual(ddt.make_params_name(3, None, Exception()), '3') + else: + self.assertEqual(ddt.make_params_name(3, None, Exception()), '3_') + + def test__unnamed_value_with__name__attribute(self): + class SampleInt(int): + pass + + v = SampleInt(1) + v.__name__ = 'custom name' + + self.assertEqual(ddt.make_params_name(3, None, v), '3_custom_name') + + def test__named_value_with__name__attribute(self): + class SampleInt(int): + pass + + v = SampleInt(1) + v.__name__ = 'custom name' + + self.assertEqual(ddt.make_params_name(3, 'myint', v), '3_myint') + + +class TestParams(TestCase): + + def test__Params_has_name_attribute(self): + p = ddt.Params('sample_parameters', [], {}) + self.assertEqual(p.name, 'sample_parameters') + + def test__Params_has_args_attribute(self): + p = ddt.Params('p', ['a', 'b', 'c'], {}) + self.assertEqual(p.args, ['a', 'b', 'c']) + + def test__Params_has_kwargs_attribute(self): + p = ddt.Params('p', [], dict(a=1, b=2, c=3)) + self.assertEqual(p.kwargs, dict(a=1, b=2, c=3)) + + def test__ParamsFailure_has_name_attribute(self): + reason = Exception() + p = ddt.ParamsFailure('error', reason) + self.assertEqual(p.name, 'error') + + def test__ParamsFailure_has_reason_attribute(self): + reason = Exception() + p = ddt.ParamsFailure('error', reason) + self.assertEqual(p.reason, reason) + + def test__Params_unpack_unpacks_lists_in_args(self): + p = ddt.Params('p', [['a', 'b'], ['c']], {}) + p.unpack() + self.assertEqual(p.args, ['a', 'b', 'c']) + + def test__Params_unpack_unpacks_tuples_in_args(self): + p = ddt.Params('p', [('a', 'b'), ('c',)], {}) + p.unpack() + self.assertEqual(p.args, ['a', 'b', 'c']) + + def test__Params_unpack_unpacks_dicts_in_args(self): + p = ddt.Params('p', [dict(a=1, b=1), dict(a=2, c=2)], dict(b=0, d=0)) + p.unpack() + self.assertEqual(p.kwargs, dict(a=2, b=1, c=2, d=0)) + + def test__Params_unpack_treats_scalars_in_args_as_singletons(self): + p = ddt.Params( + 'p', + [['a', 'b'], 'c', dict(b=1, c=1), dict(c=2, d=2)], + dict(a=0, b=0) + ) + p.unpack() + self.assertEqual(p.args, ['a', 'b', 'c']) + self.assertEqual(p.kwargs, dict(a=0, b=1, c=2, d=2)) + + def test__Params_unpack_does_nothing_with_argument_0(self): + p = ddt.Params( + 'p', + [['a', 'b'], 'c', dict(b=1, c=1), dict(c=2, d=2)], + dict(a=0, b=0) + ) + p.unpack(0) + + self.assertEqual( + p.args, + [['a', 'b'], 'c', dict(b=1, c=1), dict(c=2, d=2)] + ) + self.assertEqual(p.kwargs, dict(a=0, b=0)) + + def test__Params_unpack_unpacks_two_levels_with_argument_2(self): + p = ddt.Params( + 'p', + [[['a', 'b']], [dict(b=1, c=1)], [[dict(c=2, d=2)]]], + dict(a=0, b=0) + ) + p.unpack(2) + + self.assertEqual(p.args, ['a', 'b', dict(c=2, d=2)]) + self.assertEqual(p.kwargs, dict(a=0, b=1, c=1)) + + def test_Params_unpack_returns_self(self): + p = ddt.Params('p', ['a', 'b'], dict(a=0, b=0)) + p1 = p.unpack() + self.assertEqual(p, p1) + + def test__ParamsFailure_unpack_does_nothing(self): + reason = Exception() + p = ddt.ParamsFailure('error', reason) + p.unpack() + self.assertEqual(p.name, 'error') + self.assertEqual(p.reason, reason) + + def test_ParamsFailure_unpack_returns_self(self): + reason = Exception() + p = ddt.ParamsFailure('error', reason) + p1 = p.unpack() + self.assertEqual(p, p1) + + def test__ParamsA_plus_ParamsB_is_ParamsAB(self): + p1 = ddt.Params('p1', ['a', 'b'], dict(x=0, y=0)) + p2 = ddt.Params('p2', ['c', 'd'], dict(y=1, z=1)) + p = p1 + p2 + self.assertIsInstance(p, ddt.Params) + self.assertEqual(p.name, 'p1__p2') + self.assertEqual(p.args, ['a', 'b', 'c', 'd']) + self.assertEqual(p.kwargs, dict(x=0, y=1, z=1)) + + def test__Params_plus_ParamsFailure_is_ParamsFailure(self): + reason = Exception() + p1 = ddt.Params('p1', ['a', 'b'], dict(x=0, y=0)) + p2 = ddt.ParamsFailure('error', reason) + p = p1 + p2 + self.assertIsInstance(p, ddt.ParamsFailure) + self.assertEqual(p.name, 'p1__error') + self.assertEqual(p.reason, reason) + + def test__ParamsFailure_plus_Params_is_ParamsFailure(self): + reason = Exception() + p1 = ddt.ParamsFailure('error', reason) + p2 = ddt.Params('p2', ['a', 'b'], dict(x=0, y=0)) + p = p1 + p2 + self.assertIsInstance(p, ddt.ParamsFailure) + self.assertEqual(p.name, 'error__p2') + self.assertEqual(p.reason, reason) + + def test__ParamsFailureA_plus_ParamsFailureB_is_ParamsFailureA(self): + reason1 = Exception("Reason 1") + reason2 = Exception("Reason 2") + p1 = ddt.ParamsFailure('error1', reason1) + p2 = ddt.ParamsFailure('error2', reason2) + p = p1 + p2 + self.assertIsInstance(p, ddt.ParamsFailure) + self.assertEqual(p.name, 'error1__error2') + self.assertEqual(p.reason, reason1) + + def test__combine_names__None_None(self): + self.assertEqual(ddt.combine_names(None, None), None) + + def test__combine_names__None_value(self): + self.assertEqual(ddt.combine_names(None, 'name2'), 'name2') + + def test__combine_names__value_None(self): + self.assertEqual(ddt.combine_names('name1', None), 'name1') + + def test__combine_names__value_value(self): + self.assertEqual(ddt.combine_names('name1', 'name2'), 'name1__name2') + + +class TestParamsSet(TestCase): + + def test__InlineDataValues_generates_unnamed_and_named_Params(self): + ps = ddt.InlineDataValues('b', 'a', z='c', y='d') + + params = list(ps) + names = ['0_b', '1_a', '2_y', '3_z'] + values = [['b'], ['a'], ['d'], ['c']] + + self.assertEqual(len(values), 4) + for p, n, v in zip(params, names, values): + self.assertIsInstance(p, ddt.Params) + self.assertEqual(p.name, n) + self.assertEqual(p.args, v) + self.assertEqual(p.kwargs, {}) + + def test__FileDataValues_generates_unnamed_Params_from_list(self): + ps = ddt.FileDataValues('test_data_list.json') + ps.use_class(self.__class__) + + params = list(ps) + names = ['0_Hello', '1_Goodbye'] + values = [['Hello'], ['Goodbye']] + + self.assertEqual(len(values), 2) + for p, n, v in zip(params, names, values): + self.assertIsInstance(p, ddt.Params) + self.assertEqual(p.name, n) + self.assertEqual(p.args, v) + self.assertEqual(p.kwargs, {}) + + def test__FileDataValues_generates_named_Params_from_dict(self): + ps = ddt.FileDataValues('test_data_dict.json') + ps.use_class(self.__class__) + + params = list(ps) + names = ['0_sorted_list', '1_unsorted_list'] + values = [[[15, 12, 50]], [[10, 12, 15]]] + + self.assertEqual(len(values), 2) + for p, n, v in zip(params, names, values): + self.assertIsInstance(p, ddt.Params) + self.assertEqual(p.name, n) + self.assertEqual(p.args, v) + self.assertEqual(p.kwargs, {}) + + def test__FileDataValues_generates_ParamsFailure_if_file_not_found(self): + ps = ddt.FileDataValues('test_no_such_file.json') + ps.use_class(self.__class__) + + params = list(ps) + + self.assertEqual(len(params), 1) + self.assertIsInstance(params[0], ddt.ParamsFailure) + self.assertIsInstance(params[0].reason, IOError) + self.assertIn("No such file or directory", str(params[0].reason)) + + if six.PY2: + self.assertEqual(params[0].name, 'IOError') + elif six.PY3: + self.assertEqual(params[0].name, 'FileNotFoundError') + + def test__FileDataValues_generates_ParamsFailure_on_invalid_JSON(self): + ps = ddt.FileDataValues('test_data_invalid.json') + ps.use_class(self.__class__) + + params = list(ps) + + self.assertEqual(len(params), 1) + self.assertIsInstance(params[0], ddt.ParamsFailure) + self.assertEqual(params[0].name, 'ValueError') + self.assertIsInstance(params[0].reason, ValueError) + self.assertIn( + "Invalid control character at: line 2 column 11", + str(params[0].reason) + ) diff --git a/test/test_ddt2_specification.py b/test/test_ddt2_specification.py new file mode 100644 index 0000000..f95bb32 --- /dev/null +++ b/test/test_ddt2_specification.py @@ -0,0 +1,514 @@ + +from unittest import TestCase + +import gc +import warnings +import six + +import ddt + + +class TestDecorators(TestCase): + + def test__data_decor_adds_derived_tests_for_unnamed_values(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data('a', 'b') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a() + self.assertEqual(args, ('a',)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_b() + self.assertEqual(args, ('b',)) + self.assertEqual(kwargs, {}) + + def test__data_decor_adds_derived_tests_for_named_values(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(x='a', y='b') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_x() + self.assertEqual(args, ('a',)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_y() + self.assertEqual(args, ('b',)) + self.assertEqual(kwargs, {}) + + def test__data_decor_adds_derived_tests_for_combined_values(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data('A', x='B') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A() + self.assertEqual(args, ('A',)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_x() + self.assertEqual(args, ('B',)) + self.assertEqual(kwargs, {}) + + def test__unpack_unpacks_list_in_single_value_set(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.data(a=['A', 'B']) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a() + self.assertEqual(args, ('A', 'B')) + self.assertEqual(kwargs, {}) + + def test__unpack_unpacks_tuple_in_single_value_set(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.data(a=('A', 'B')) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a() + self.assertEqual(args, ('A', 'B')) + self.assertEqual(kwargs, {}) + + def test__unpack_preserves_scalar_in_single_value_set(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.data(a='A') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a() + self.assertEqual(args, ('A',)) + self.assertEqual(kwargs, {}) + + def test__unpack_converts_dict_to_kwargs_in_single_value_set(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.data(a=dict(y='A', x='B')) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a() + self.assertEqual(args, ()) + self.assertEqual(kwargs, dict(x='B', y='A')) + + def test__nested_data_decorators_produce_cartesian_product(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data('a', 'b') + @ddt.data('c', 'd') + @ddt.data('e') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a__0_c__0_e() + self.assertEqual(args, ('a', 'c', 'e')) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__0_a__1_d__0_e() + self.assertEqual(args, ('a', 'd', 'e')) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_b__0_c__0_e() + self.assertEqual(args, ('b', 'c', 'e')) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_b__1_d__0_e() + self.assertEqual(args, ('b', 'd', 'e')) + self.assertEqual(kwargs, {}) + + def test__unpack_unpacks_only_the_next_data_decorator(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(A=['a', 'b']) + @ddt.unpack + @ddt.data(B=['c', 'd']) + @ddt.data(C=dict(x='e', y='f')) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C() + self.assertEqual(args, (['a', 'b'], 'c', 'd', dict(x='e', y='f'))) + self.assertEqual(kwargs, {}) + + def test__multiple_unpack_decorators_unpack_nested_lists(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(A=['a', 'b']) + @ddt.unpack + @ddt.unpack + @ddt.data(B=[('c', 'd')]) + @ddt.data(C=dict(x='e', y='f')) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C() + self.assertEqual(args, (['a', 'b'], 'c', 'd', dict(x='e', y='f'))) + self.assertEqual(kwargs, {}) + + def test__multiple_unpack_decorators_are_idempotent_on_dicts(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(A=['a', 'b']) + @ddt.unpack + @ddt.unpack + @ddt.data(B=dict(r='c', s='d')) + @ddt.data(C=dict(x='e', y='f')) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C() + self.assertEqual(args, (['a', 'b'], dict(x='e', y='f'))) + self.assertEqual(kwargs, dict(r='c', s='d')) + + def test__multiple_unpacked_dicts_are_combined(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(A=dict(a=0, b=0)) + @ddt.unpack + @ddt.data(B=dict(b=1, c=1)) + @ddt.unpack + @ddt.unpack + @ddt.data(C=[dict(c=2, d=2)]) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C() + self.assertEqual(args, (dict(a=0, b=0),)) + self.assertEqual(kwargs, dict(b=1, c=2, d=2)) + + def test__unpackall_unpacks_data_from_all_decorators(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpackall + @ddt.data(A=['a', 'b']) + @ddt.data(B=dict(a=0, b=0)) + @ddt.data(C=[dict(b=1, c=1)]) + @ddt.data(D=[['c', 'd']]) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C__0_D() + self.assertEqual(args, ('a', 'b', dict(b=1, c=1), ['c', 'd'])) + self.assertEqual(kwargs, dict(a=0, b=0)) + + def test__position_of_unpackall_does_not_matter(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(A=['a', 'b']) + @ddt.data(B=dict(a=0, b=0)) + @ddt.data(C=[dict(b=1, c=1)]) + @ddt.data(D=[['c', 'd']]) + @ddt.unpackall + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C__0_D() + self.assertEqual(args, ('a', 'b', dict(b=1, c=1), ['c', 'd'])) + self.assertEqual(kwargs, dict(a=0, b=0)) + + def test__multiple_unpackall_decorators_unpack_nested_lists(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpackall + @ddt.data(A=['a', 'b']) + @ddt.data(B=dict(a=0, b=0)) + @ddt.data(C=[dict(b=1, c=1)]) + @ddt.data(D=[['c', 'd']]) + @ddt.unpackall + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C__0_D() + self.assertEqual(args, ('a', 'b', 'c', 'd')) + self.assertEqual(kwargs, dict(a=0, b=1, c=1)) + + def test__unpack_and_unpackall_decorators_combine_as_expected(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpackall + @ddt.data(A=[['a', 'b']]) + @ddt.data(B=[dict(a=0, b=0)]) + @ddt.unpack + @ddt.data(C=[dict(b=1, c=1)]) + @ddt.unpack + @ddt.data(D=[['c', 'd']]) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_A__0_B__0_C__0_D() + self.assertEqual(args, (['a', 'b'], dict(a=0, b=0), 'c', 'd')) + self.assertEqual(kwargs, dict(b=1, c=1)) + + def test__names_for_complex_values_are_generated_correctly(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(dict(a=0, b=0)) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + if ddt.is_hash_randomized(): + args, kwargs = tc.test__0() + else: + try: + args, kwargs = tc.test__0_a_0_b_0() + except AttributeError: + args, kwargs = tc.test__0_b_0_a_0() + + self.assertEqual(args, (dict(a=0, b=0),)) + self.assertEqual(kwargs, {}) + + def test__test_name_uses__name__attribute_if_complex_value_has_it(self): + + class SampleInt(int): + pass + + d1 = SampleInt(1) + d1.__name__ = 'custom name' + + d2 = SampleInt(2) + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(d1, d2) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_custom_name() + self.assertEqual(args, (1,)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_2() + self.assertEqual(args, (2,)) + self.assertEqual(kwargs, {}) + + def test__sequences_of_underscores_in_test_names_are_reduced_to_one(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.data(['a', 'b']) + @ddt.data((1, 2)) + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_a_b__0_1_2() + self.assertEqual(args, (['a', 'b'], (1, 2))) + self.assertEqual(kwargs, {}) + + def test__file_data_decor_adds_derived_tests_with_unnamed_values(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.file_data('test_data_list.json') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_Hello() + self.assertEqual(args, ('Hello',)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_Goodbye() + self.assertEqual(args, ('Goodbye',)) + self.assertEqual(kwargs, {}) + + def test__file_data_decor_adds_derived_tests_for_named_values(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.file_data('test_data_dict.json') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_sorted_list() + self.assertEqual(args, ([15, 12, 50],)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_unsorted_list() + self.assertEqual(args, ([10, 12, 15],)) + self.assertEqual(kwargs, {}) + + def test__unpack_works_for_file_data_decor_too(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.file_data('test_data_dict.json') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + args, kwargs = tc.test__0_sorted_list() + self.assertEqual(args, (15, 12, 50)) + self.assertEqual(kwargs, {}) + + args, kwargs = tc.test__1_unsorted_list() + self.assertEqual(args, (10, 12, 15)) + self.assertEqual(kwargs, {}) + + def test__file_data_decor_does_not_cause_resource_warning(self): + # ResourceWarning does not exist in Python 2 (?) + if six.PY3: + # let's violate priniciples of PEP8 to become pep8-compliant :-( + # works around F821 in Python2 + resource_warning = eval('ResourceWarning') + with warnings.catch_warnings(record=True) as w: + + warnings.resetwarnings() # clear all filters + warnings.simplefilter('ignore') # ignore all + warnings.simplefilter('always', resource_warning) # add filter + + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.file_data('test_data_dict.json') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + del tc + + gc.collect() + + self.assertEqual(list(w), []) + + def test__tests_with_file_data_raise_exceptions_on_file_not_found(self): + @ddt.ddt + class SampleTestCase(object): + @ddt.file_data('test_no_such_file.json') + @ddt.data('a', 'b') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + if six.PY2: + with self.assertRaises(IOError): + tc.test__IOError__0_a() + + with self.assertRaises(IOError): + tc.test__IOError__1_b() + + if six.PY3: + with self.assertRaises(IOError): + tc.test__FileNotFoundError__0_a() + + with self.assertRaises(IOError): + tc.test__FileNotFoundError__1_b() + + def test__tests_with_file_data_raise_exceptions_on_invalid_json(self): + @ddt.ddt + class SampleTestCase(object): + @ddt.file_data('test_data_invalid.json') + @ddt.data('a', 'b') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + with self.assertRaises(ValueError): + tc.test__ValueError__0_a() + + with self.assertRaises(ValueError): + tc.test__ValueError__1_b() + + def test__unpacking_is_safe_even_if_loading_file_data_fails(self): + @ddt.ddt + class SampleTestCase(object): + @ddt.unpack + @ddt.file_data('test_data_invalid.json') + @ddt.data('a', 'b') + @ddt.unpackall + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + with self.assertRaises(ValueError): + tc.test__ValueError__0_a() + + with self.assertRaises(ValueError): + tc.test__ValueError__1_b() + + def test__file_data_supports_file_encoding(self): + + @ddt.ddt + class SampleTestCase(object): + @ddt.file_data('test_data_utf8sig.json', encoding='utf-8-sig') + def test(self, *args, **kwargs): + return args, kwargs + + tc = SampleTestCase() + + method_name = 'test__0_u0158' if six.PY2 else 'test__0_\u0158' + self.assertTrue(hasattr(tc, method_name)) diff --git a/test/test_example.py b/test/test_example.py index 35fc002..903df9e 100644 --- a/test/test_example.py +++ b/test/test_example.py @@ -39,13 +39,13 @@ def test_file_data_dict(self, value): def test_file_data_list(self, value): self.assertTrue(is_a_greeting(value)) - @data((3, 2), (4, 3), (5, 3)) @unpack + @data((3, 2), (4, 3), (5, 3)) def test_tuples_extracted_into_arguments(self, first_value, second_value): self.assertTrue(first_value > second_value) - @data([3, 2], [4, 3], [5, 3]) @unpack + @data([3, 2], [4, 3], [5, 3]) def test_list_extracted_into_arguments(self, first_value, second_value): self.assertTrue(first_value > second_value) diff --git a/test/test_example01.py b/test/test_example01.py new file mode 100644 index 0000000..f2e9c9b --- /dev/null +++ b/test/test_example01.py @@ -0,0 +1,17 @@ +import unittest +from test.mycode import larger_than_two + + +class FooTestCase(unittest.TestCase): + + def test_manual_neg1(self): + self.assertFalse(larger_than_two(-1)) + + def test_manual_zero(self): + self.assertFalse(larger_than_two(0)) + + def test_manual_pos2(self): + self.assertFalse(larger_than_two(2)) + + def test_manual_pos3(self): + self.assertTrue(larger_than_two(3)) diff --git a/test/test_example02.py b/test/test_example02.py new file mode 100644 index 0000000..9201631 --- /dev/null +++ b/test/test_example02.py @@ -0,0 +1,17 @@ + +import unittest +from ddt import ddt, data +from test.mycode import larger_than_two + + +@ddt +class FooTestCase(unittest.TestCase): + + @data( + [-1, False], + [0, False], + [2, False], + [3, True] + ) + def test_decorated(self, data): + self.assertEqual(larger_than_two(data[0]), data[1]) diff --git a/test/test_example03.py b/test/test_example03.py new file mode 100644 index 0000000..242a9f8 --- /dev/null +++ b/test/test_example03.py @@ -0,0 +1,17 @@ + +import unittest +from ddt import ddt, data +from test.mycode import larger_than_two + + +@ddt +class FooTestCase(unittest.TestCase): + + @data( + neg1=[-1, False], + zero=[0, False], + pos2=[2, False], + pos3=[3, True], + ) + def test_decorated(self, data): + self.assertEqual(larger_than_two(data[0]), data[1]) diff --git a/test/test_example04.py b/test/test_example04.py new file mode 100644 index 0000000..b274c42 --- /dev/null +++ b/test/test_example04.py @@ -0,0 +1,18 @@ + +import unittest +from ddt import ddt, data, unpack +from test.mycode import larger_than_two + + +@ddt +class FooTestCase(unittest.TestCase): + + @unpack + @data( + neg1=[-1, False], + zero=[0, False], + pos2=[2, False], + pos3=[3, True], + ) + def test_decorated(self, value, result): + self.assertEqual(larger_than_two(value), result) diff --git a/test/test_example05.py b/test/test_example05.py new file mode 100644 index 0000000..56e9ebd --- /dev/null +++ b/test/test_example05.py @@ -0,0 +1,18 @@ + +import unittest +from ddt import ddt, data, unpack +from test.mycode import larger_than_two + + +@ddt +class FooTestCase(unittest.TestCase): + + @unpack + @data( + neg1={"value": -1, "result": False}, + zero={"value": 0, "result": False}, + pos2={"value": 2, "result": False}, + pos3={"value": 3, "result": True}, + ) + def test_decorated(self, value, result): + self.assertEqual(larger_than_two(value), result) diff --git a/test/test_example06.py b/test/test_example06.py new file mode 100644 index 0000000..feb2a95 --- /dev/null +++ b/test/test_example06.py @@ -0,0 +1,19 @@ + +import unittest +from ddt import ddt, data, unpack +from test.mycode import larger_than_two + + +@ddt +class FooTestCase(unittest.TestCase): + + @unpack + @unpack + @data( + base2_10=[[False, "10"], {"base": 2}], + base3_10=[[True, "10"], {"base": 3}], + base10_10=[[True, "10"], {"base": 10}], + default_10=[[True, "10"], {}], + ) + def test_decorated(self, result, value, **kwargs): + self.assertEqual(larger_than_two(value, **kwargs), result) diff --git a/test/test_example07.py b/test/test_example07.py new file mode 100644 index 0000000..c30849f --- /dev/null +++ b/test/test_example07.py @@ -0,0 +1,20 @@ + +import unittest +from ddt import ddt, file_data, unpack +from test.mycode import larger_than_two + + +@ddt +class FooTestCase(unittest.TestCase): + + @unpack + @unpack + @file_data("json_list.json", encoding="ascii") + def test_with_json_list(self, result, value, **kwargs): + self.assertEqual(larger_than_two(value, **kwargs), result) + + @unpack + @unpack + @file_data("json_dict.json") + def test_with_json_dict(self, result, value, **kwargs): + self.assertEqual(larger_than_two(value, **kwargs), result) diff --git a/test/test_example08.py b/test/test_example08.py new file mode 100644 index 0000000..ec4880f --- /dev/null +++ b/test/test_example08.py @@ -0,0 +1,11 @@ +import unittest +from ddt import ddt, data + + +@ddt +class FooTestCase(unittest.TestCase): + + @data("a", "_", "B", "1") + @data("b", "*", "X", "2", "") + def test_decorated(self, a, b): + self.assertEqual((a + b).lower(), a.lower() + b.lower()) diff --git a/test/test_example09.py b/test/test_example09.py new file mode 100644 index 0000000..a5744e6 --- /dev/null +++ b/test/test_example09.py @@ -0,0 +1,23 @@ +import unittest +from ddt import ddt, data, unpack + + +@ddt +class FooTestCase(unittest.TestCase): + + @unpack + @data( + no_options=[[]], + with_sep_None=[[None]], + with_sep_None_maxsplit_neg1=[[None, -1]], + ) + @unpack + @unpack + @data( + empty_string=[[""], {"result": []}], + only_spaces=[[" "], {"result": []}], + two_words=[[" ab cd "], {"result": ["ab", "cd"]}], + three_words=[["ab cd ef"], {"result": ["ab", "cd", "ef"]}], + ) + def test_decorated(self, args, value, result=None): + self.assertEqual(value.split(*args), result) diff --git a/test/test_example10.py b/test/test_example10.py new file mode 100644 index 0000000..7d6ddab --- /dev/null +++ b/test/test_example10.py @@ -0,0 +1,21 @@ +import unittest +from ddt import ddt, data, unpackall + + +@ddt +class FooTestCase(unittest.TestCase): + + @unpackall + @data( + empty_string=[[], ""], + only_spaces=[[], " "], + two_words=[["ab", "cd"], " ab cd "], + three_words=[["ab", "cd", "ef"], "ab cd ef"], + ) + @data( + no_options=[], + with_sep_None=[None], + with_sep_None_maxsplit_neg1=[None, -1], + ) + def test_decorated(self, result, value, *args): + self.assertEqual(value.split(*args), result) diff --git a/test/test_functional.py b/test/test_functional.py index b0e8cfd..15780b2 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -71,7 +71,7 @@ def hello(): extra_attrs = dh_keys - keys assert_equal(len(extra_attrs), 1) extra_attr = extra_attrs.pop() - assert_equal(getattr(data_hello, extra_attr), (1, 2)) + assert_equal(getattr(data_hello, extra_attr)[0].unnamed_values, (1, 2)) def test_file_data_decorator_with_dict(): @@ -84,7 +84,7 @@ def hello(): pre_size = len(hello.__dict__) keys = set(hello.__dict__.keys()) - data_hello = data("test_data_dict.json")(hello) + data_hello = file_data("test_data_dict.json")(hello) dh_keys = set(data_hello.__dict__.keys()) post_size = len(data_hello.__dict__) @@ -93,7 +93,10 @@ def hello(): extra_attrs = dh_keys - keys assert_equal(len(extra_attrs), 1) extra_attr = extra_attrs.pop() - assert_equal(getattr(data_hello, extra_attr), ("test_data_dict.json",)) + assert_equal( + getattr(data_hello, extra_attr)[0].filepath, + "test_data_dict.json" + ) is_test = lambda x: x.startswith('test_') @@ -130,8 +133,8 @@ def test_file_data_test_names_dict(): test_data_path = os.path.join(tests_dir, 'test_data_dict.json') test_data = json.loads(open(test_data_path).read()) created_tests = set([ - "test_something_again_{0}_{1}".format(index + 1, name) - for index, name in enumerate(test_data.keys()) + "test_something_again__{0}_{1}".format(index, name) + for index, name in enumerate(sorted(test_data.keys())) ]) assert_equal(tests, created_tests) @@ -169,14 +172,14 @@ def test_feed_data_file_data(): def test_feed_data_file_data_missing_json(): """ - Test that a ValueError is raised + Test that an IOError is raised """ tests = filter(is_test, FileDataMissingDummy.__dict__) obj = FileDataMissingDummy() for test in tests: method = getattr(obj, test) - assert_raises(ValueError, method) + assert_raises(IOError, method) def test_ddt_data_name_attribute(): @@ -202,8 +205,8 @@ class Mytest(object): setattr(Mytest, 'test_hello', data_hello) ddt_mytest = ddt(Mytest) - assert_is_not_none(getattr(ddt_mytest, 'test_hello_1_data1')) - assert_is_not_none(getattr(ddt_mytest, 'test_hello_2_2')) + assert_is_not_none(getattr(ddt_mytest, 'test_hello__0_data1')) + assert_is_not_none(getattr(ddt_mytest, 'test_hello__1_2')) def test_ddt_data_unicode(): @@ -224,13 +227,13 @@ class Mytest(object): def test_hello(self, val): pass - assert_is_not_none(getattr(Mytest, 'test_hello_1_ascii')) - assert_is_not_none(getattr(Mytest, 'test_hello_2_non_ascii__u2603')) + assert_is_not_none(getattr(Mytest, 'test_hello__0_ascii')) + assert_is_not_none(getattr(Mytest, 'test_hello__1_non_ascii_u2603')) if is_hash_randomized(): - assert_is_not_none(getattr(Mytest, 'test_hello_3')) + assert_is_not_none(getattr(Mytest, 'test_hello__2')) else: assert_is_not_none(getattr(Mytest, - 'test_hello_3__u__u2603____data__')) + 'test_hello__2_u_u2603_data')) elif six.PY3: @@ -240,12 +243,12 @@ class Mytest(object): def test_hello(self, val): pass - assert_is_not_none(getattr(Mytest, 'test_hello_1_ascii')) - assert_is_not_none(getattr(Mytest, 'test_hello_2_non_ascii__')) + assert_is_not_none(getattr(Mytest, 'test_hello__0_ascii')) + assert_is_not_none(getattr(Mytest, 'test_hello__1_non_ascii')) if is_hash_randomized(): - assert_is_not_none(getattr(Mytest, 'test_hello_3')) + assert_is_not_none(getattr(Mytest, 'test_hello__2')) else: - assert_is_not_none(getattr(Mytest, 'test_hello_3________data__')) + assert_is_not_none(getattr(Mytest, 'test_hello__2_data')) def test_feed_data_with_invalid_identifier(): @@ -259,6 +262,6 @@ def test_feed_data_with_invalid_identifier(): method = getattr(obj, tests[0]) assert_equal( method.__name__, - 'test_data_with_invalid_identifier_1_32v2_g__Gmw845h_W_b53wi_' + 'test_data_with_invalid_identifier__0_32v2_g_Gmw845h_W_b53wi' ) assert_equal(method(), '32v2 g #Gmw845h$W b53wi.')