diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b98e71e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: + #- 2.6 + - 2.7 + - 3.2 +install: pip install -r test-requirements.txt +script: + - nosetests --with-cov --cover-package=ddt diff --git a/README b/README.md similarity index 57% rename from README rename to README.md index d480ca0..933f0ce 100644 --- a/README +++ b/README.md @@ -1,5 +1,12 @@ +[![Build Status](https://travis-ci.org/txels/ddt.png)](https://travis-ci.org/bulkan/ddt) + 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. +Installation +------------ + +```pip install ddt``` + More info at http://ddt.readthedocs.org/ diff --git a/build.sh b/build.sh index 102c2a7..62b13a9 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,4 @@ #!/bin/bash nosetests --with-coverage --cover-html flake8 ddt.py test || echo "Flake8 errors" +(cd docs; make html) diff --git a/ddt.py b/ddt.py index 758c682..c17506a 100644 --- a/ddt.py +++ b/ddt.py @@ -1,8 +1,15 @@ +import inspect +import json +import os from functools import wraps -__version__ = '0.3.0' +__version__ = '0.4.0' -MAGIC = '%values' # this value cannot conflict with any real python attribute +# this value cannot conflict with any real python attribute +DATA_ATTR = '%values' + +# store the path to JSON file +FILE_ATTR = '%file_path' def data(*values): @@ -12,7 +19,31 @@ def data(*values): Should be added to methods of instances of ``unittest.TestCase``. """ def wrapper(func): - setattr(func, MAGIC, values) + setattr(func, DATA_ATTR, values) + return func + return wrapper + + +def file_data(value): + """ + Method decorator to add to your test methods. + + 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. + + 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. + """ + def wrapper(func): + setattr(func, FILE_ATTR, value) return func return wrapper @@ -30,6 +61,15 @@ def ddt(cls): The names of the test methods follow the pattern ``test_func_name + "_" + str(data)``. If ``data.__name__`` exists, it is used instead for the test method name. + + 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. + + The names of these test methods follow the pattern of + ``test_name`` + str(data)`` """ def feed_data(func, *args, **kwargs): @@ -41,10 +81,31 @@ def ddt(cls): return func(self, *args, **kwargs) return wrapper - for name, f in list(cls.__dict__.items()): - if hasattr(f, MAGIC): - for i, v in enumerate(getattr(f, MAGIC)): + def process_file_data(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) + if os.path.exists(data_file_path): + data = json.loads(open(data_file_path).read()) + for elem in data: + if isinstance(data, dict): + key, value = elem, data[elem] + test_name = "{0}_{1}".format(name, key) + elif isinstance(data, list): + value = elem + test_name = "{0}_{1}".format(name, value) + setattr(cls, test_name, feed_data(func, value)) + + for name, func in list(cls.__dict__.items()): + if hasattr(func, DATA_ATTR): + for v in getattr(func, DATA_ATTR): test_name = getattr(v, "__name__", "{0}_{1}".format(name, v)) - setattr(cls, test_name, feed_data(f, v)) + setattr(cls, test_name, feed_data(func, v)) + delattr(cls, name) + elif hasattr(func, FILE_ATTR): + file_attr = getattr(func, FILE_ATTR) + process_file_data(name, func, file_attr) delattr(cls, name) return cls diff --git a/docs/example.rst b/docs/example.rst index 4d1aa6d..28b4a0b 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -2,20 +2,48 @@ Example usage ============= DDT consists of a class decorator ``ddt`` (for your ``TestCase`` subclass) -and a method decorator ``data`` (for your tests that want to be multiplied). +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. 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:: - % nosetests test_example.py - .......... + $ nosetests -v test/test_example.py + test_10_greater_than_5 (test.test_example.FooTestCase) ... ok + test_2_greater_than_1 (test.test_example.FooTestCase) ... ok + test_file_data_dict_sorted_list (test.test_example.FooTestCase) ... ok + test_file_data_dict_unsorted_list (test.test_example.FooTestCase) ... ok + test_file_data_list_Goodbye (test.test_example.FooTestCase) ... ok + test_file_data_list_Hello (test.test_example.FooTestCase) ... ok + test_larger_than_two_12 (test.test_example.FooTestCase) ... ok + test_larger_than_two_23 (test.test_example.FooTestCase) ... ok + test_larger_than_two_3 (test.test_example.FooTestCase) ... ok + test_larger_than_two_4 (test.test_example.FooTestCase) ... ok + test_not_larger_than_two_-3 (test.test_example.FooTestCase) ... ok + test_not_larger_than_two_0 (test.test_example.FooTestCase) ... ok + test_not_larger_than_two_1 (test.test_example.FooTestCase) ... ok + test_not_larger_than_two_2 (test.test_example.FooTestCase) ... ok + test_undecorated (test.test_example.FooTestCase) ... ok + ---------------------------------------------------------------------- - Ran 10 tests in 0.002s + Ran 15 tests in 0.002s OK -3 test methods + some *magic* decorators = 10 test cases. +6 test methods + some *magic* decorators = 15 test cases. diff --git a/test/mycode.py b/test/mycode.py index 4eb0af4..bd562c8 100644 --- a/test/mycode.py +++ b/test/mycode.py @@ -1,2 +1,15 @@ +""" +Some simple functions that we will use in our tests. +""" + + def larger_than_two(value): return value > 2 + + +def has_three_elements(value): + return len(value) == 3 + + +def is_a_greeting(value): + return value in ['Hello', 'Goodbye'] diff --git a/test/test_data_dict.json b/test/test_data_dict.json new file mode 100644 index 0000000..a220e80 --- /dev/null +++ b/test/test_data_dict.json @@ -0,0 +1,4 @@ +{ + "unsorted_list": [ 10, 12, 15 ], + "sorted_list": [ 15, 12, 50 ] +} diff --git a/test/test_example.py b/test/test_example.py index 23c747c..1ea09bf 100644 --- a/test/test_example.py +++ b/test/test_example.py @@ -1,6 +1,6 @@ import unittest -from ddt import ddt, data -from .mycode import larger_than_two +from ddt import ddt, data, file_data +from .mycode import larger_than_two, has_three_elements, is_a_greeting class mylist(list): @@ -16,6 +16,9 @@ def annotated(a, b): @ddt class FooTestCase(unittest.TestCase): + def test_undecorated(self): + self.assertTrue(larger_than_two(24)) + @data(3, 4, 12, 23) def test_larger_than_two(self, value): self.assertTrue(larger_than_two(value)) @@ -28,3 +31,11 @@ class FooTestCase(unittest.TestCase): def test_greater(self, value): a, b = value self.assertGreater(a, b) + + @file_data('test_data_dict.json') + def test_file_data_dict(self, value): + self.assertTrue(has_three_elements(value)) + + @file_data('test_data_list.json') + def test_file_data_list(self, value): + self.assertTrue(is_a_greeting(value)) diff --git a/test/test_functional.py b/test/test_functional.py index 26478d9..afb0183 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -1,18 +1,35 @@ -from ddt import ddt, data +import os +import json +from ddt import ddt, data, file_data from nose.tools import assert_equal, assert_is_not_none @ddt class Dummy(object): - """Dummy class to test decorators on""" + """ + Dummy class to test the data decorator on + """ @data(1, 2, 3, 4) def test_something(self, value): return value +@ddt +class FileDataDummy(object): + """ + Dummy class to test the file_data decorator on + """ + + @file_data("test_data_dict.json") + def test_something_again(self, value): + return value + + def test_data_decorator(): - """Test the ``data`` method decorator""" + """ + Test the ``data`` method decorator + """ def hello(): pass @@ -30,20 +47,74 @@ def test_data_decorator(): assert_equal(getattr(data_hello, extra_attr), (1, 2)) +def test_file_data_decorator_with_dict(): + """ + Test the ``file_data`` method decorator + """ + + def hello(): + pass + + pre_size = len(hello.__dict__) + keys = set(hello.__dict__.keys()) + data_hello = data("test_data_dict.json")(hello) + + dh_keys = set(data_hello.__dict__.keys()) + post_size = len(data_hello.__dict__) + + assert_equal(post_size, pre_size + 1) + 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",)) + + is_test = lambda x: x.startswith('test_') def test_ddt(): - """Test the ``ddt`` class decorator""" - + """ + Test the ``ddt`` class decorator + """ tests = len(list(filter(is_test, Dummy.__dict__))) assert_equal(tests, 4) -def test_feed_data(): - """Test that data is fed to the decorated tests""" +def test_file_data_test_creation(): + """ + Test that the ``file_data`` decorator creates two tests + """ + tests = len(list(filter(is_test, FileDataDummy.__dict__))) + assert_equal(tests, 2) + + +def test_file_data_test_names_dict(): + """ + Test that ``file_data`` creates tests with the correct name + + Name is the the function name plus the key in the JSON data, + when it is parsed as a dictionary. + """ + + tests = set(filter(is_test, FileDataDummy.__dict__)) + + tests_dir = os.path.dirname(__file__) + 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}".format(name) for name in test_data.keys() + ]) + + assert_equal(tests, created_tests) + + +def test_feed_data_data(): + """ + Test that data is fed to the decorated tests + """ tests = filter(is_test, Dummy.__dict__) + values = [] obj = Dummy() for test in tests: @@ -53,6 +124,21 @@ def test_feed_data(): assert_equal(set(values), set([1, 2, 3, 4])) +def test_feed_data_file_data(): + """ + Test that data is fed to the decorated tests from a file + """ + tests = filter(is_test, FileDataDummy.__dict__) + + values = [] + obj = FileDataDummy() + for test in tests: + method = getattr(obj, test) + values.extend(method()) + + assert_equal(set(values), set([10, 12, 15, 15, 12, 50])) + + def test_ddt_data_name_attribute(): """ Test the ``__name__`` attribute handling of ``data`` items with ``ddt``