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

Cartesian products and more powerful unpacking #29

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
559 changes: 432 additions & 127 deletions ddt.py

Large diffs are not rendered by default.

57 changes: 0 additions & 57 deletions docs/example.rst

This file was deleted.

9 changes: 4 additions & 5 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
213 changes: 213 additions & 0 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions test/json_dict.json
Original file line number Diff line number Diff line change
@@ -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" ], {} ]
}

6 changes: 6 additions & 0 deletions test/json_list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
[ [ false, "10" ], { "base": 2} ],
[ [ true, "10" ], { "base": 3 } ],
[ [ true, "10" ], { "base": 10 } ],
[ [ true, "10" ], { } ]
]
6 changes: 5 additions & 1 deletion test/mycode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
3 changes: 3 additions & 0 deletions test/test_data_invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"Hello

3 changes: 3 additions & 0 deletions test/test_data_utf8sig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"Ř"
]
Loading