Merge pull request #8 from txels/bulkan_file_data
Add file_data decorator
This commit is contained in:
8
.travis.yml
Normal file
8
.travis.yml
Normal 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
|
||||
@@ -1,5 +1,12 @@
|
||||
[](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/
|
||||
1
build.sh
1
build.sh
@@ -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
75
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
4
test/test_data_dict.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"unsorted_list": [ 10, 12, 15 ],
|
||||
"sorted_list": [ 15, 12, 50 ]
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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``
|
||||
|
||||
Reference in New Issue
Block a user