Merge pull request #8 from txels/bulkan_file_data

Add file_data decorator
This commit is contained in:
Carles Barrobés i Meix
2013-02-25 16:14:32 -08:00
9 changed files with 240 additions and 21 deletions

8
.travis.yml Normal file
View File

@@ -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

View File

@@ -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/

View File

@@ -1,3 +1,4 @@
#!/bin/bash
nosetests --with-coverage --cover-html
flake8 ddt.py test || echo "Flake8 errors"
(cd docs; make html)

75
ddt.py
View File

@@ -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

View File

@@ -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.

View File

@@ -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']

4
test/test_data_dict.json Normal file
View File

@@ -0,0 +1,4 @@
{
"unsorted_list": [ 10, 12, 15 ],
"sorted_list": [ 15, 12, 50 ]
}

View File

@@ -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))

View File

@@ -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``