Skip to content

Commit

Permalink
[REFACTOR, TESTS] Change mechanism for data-driven-tests
Browse files Browse the repository at this point in the history
Change to a much nicer decorater based data-driven-testing mechanism.

Now, instead of 2 functions (one for test-logic and one
containing/yielding tests), we will use a single decorated function and
an iterable. The iterable is passed to the decorator along with other
description-related arguments. The decorator does some magic (similar to
the one we did earlier) and returns a generator function that yields out
tests.

This approach is cleaner IMO as it makes the "data" of the test separate
from the logic while using only one function.
  • Loading branch information
pradyunsg committed Jun 20, 2015
1 parent 8720f87 commit 3456ede
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 52 deletions.
51 changes: 39 additions & 12 deletions py2c/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import inspect
import warnings
import traceback
from functools import partial
from functools import partial, wraps

from unittest import mock
from nose.tools import nottest, assert_in, assert_not_in
from nose.tools import istest, nottest, assert_in, assert_not_in

__all__ = ["Test", "mock", "runmodule"]

Expand Down Expand Up @@ -60,16 +60,6 @@ def __init__(self):
warnings.warn("Test subclasses' name should start with 'Test'")
super().__init__()

@nottest
def yield_tests(self, test_method, args, described=False, prefix=""):
for test_args in args:
if described:
func = partial(test_method, *test_args[1:])
func.description = prefix + test_args[0]
else:
func = partial(test_method, *test_args[:])
yield func

def assert_error_message_contains(self, error, required_phrases):
msg = error.args[0]
for word in required_phrases:
Expand All @@ -86,6 +76,43 @@ def fail(self, message="", cause=None):
raise AssertionError(message) from cause


# -----------------------------------------------------------------------------
# Data Driven Tests
# -----------------------------------------------------------------------------
@nottest
def data_driven_test(data, described=False, prefix="", suffix=""):
"""Run a test for all the provided data
Usage::
data = [['1'], ['2']]
@data_driven(data)
def decorated(*args):
print(args)
for func in decorated():
func()
"""

def decorator(function):
@istest # MARK:: Should this be here?
@wraps(function)
def wrapped(*args):
nonlocal data, described, prefix, suffix
for test_data in data:
if described:
func = partial(function, *(list(args) + list(test_data)[1:])) # noqa
func.description = prefix + test_data[0] + suffix
else:
func = partial(function, *(list(args) + list(test_data)))
print(func)
yield func
return wrapped

return decorator


def runmodule(capture=True):
"""A shorthand for running tests in test modules
"""
Expand Down
115 changes: 75 additions & 40 deletions py2c/tests/test_tests.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,96 @@
"""Unit-tests for `py2c.tests.Test`
"""Unit-tests for `py2c.tests`
"""

from nose.tools import assert_equal, assert_raises
from nose.tools import assert_equal, assert_raises, assert_is
from py2c.tests import Test, data_driven_test

from py2c.tests import Test

class TestDataDriven(Test):
"""py2c.tests.data_driven_test
"""

def check(a, b):
return a + b
def test_does_not_yield_values_with_no_arguments(self):
@data_driven_test([])
def func():
pass

for test in func():
assert False

def check_order(a, b):
return a, b
def test_yields_values_in_correct_order_given_arguments(self):
@data_driven_test([(0,), (1,), (2,)])
def func(a):
return a

iterations = 0
for test in func():
assert_equal(test(), iterations)
iterations += 1

# I know that the name's horrible but it's how all tests have been named...
class TestYieldTests(Test):
"""py2c.tests.Test.yield_tests
"""
assert_equal(iterations, 3)

def test_does_not_yield_values_with_empty_list(self):
for test in self.yield_tests(check, []):
assert False
def test_is_a_generator(self):
@data_driven_test([[]])
def func():
return 1

def test_does_yield_values_with_non_empty_list(self):
for test in self.yield_tests(check, [("bar", "baz")]):
assert_equal(test(), "barbaz")
obj = func()

def test_does_order_arguments_correctly(self):
li = [(0, 3), [0, 4]]
for test, args in zip(self.yield_tests(check_order, li), li):
assert_equal(list(test()), list(args))
assert_equal(next(obj)(), 1)
with assert_raises(StopIteration):
next(obj)

def test_is_a_generator(self):
tests = self.yield_tests(check, [[2, 2]])
def test_attaches_proper_description(self):
test_data = [["Test 0", "argument"], ["Test 1", "argument"]]

assert_equal(next(tests)(), 4)
with assert_raises(StopIteration):
next(tests)
@data_driven_test(test_data, True)
def func(arg):
assert_equal(arg, "argument")

for i, test in enumerate(func()):
assert_equal(test.description, "Test " + str(i))
test()

def test_attaches_prefix_and_suffix_to_description_correctly(self):
test_data = [["Test 0", "argument"], ["Test 1", "argument"]]

@data_driven_test(test_data, described=True, prefix="<", suffix=">")
def func(arg):
assert_equal(arg, "argument")

for i, test in enumerate(func()):
assert_equal(test.description, "<Test {}>".format(i))
test()

def test_passes_argument_called_with_to_test_function_when_described(self):
# This is needed for methods, where "self" argument is passed to
# the test function
stub = object()

test_data = [["Test 0", "argument"], ["Test 1", "argument"]]

@data_driven_test(test_data, described=True)
def func(passed, arg):
assert_is(passed, stub)
assert_equal(arg, "argument")

for i, test in enumerate(func(stub)):
test()

def test_does_attach_proper_description(self):
tests = self.yield_tests(check, [
["Test 0", "some_argument"],
("Test 1", "some_argument"),
], described=True)
def test_does_pass_argument_in_call_to_test_function_when_not_described(self): # noqa
# This is needed for methods, where "self" argument is passed to
# the test function
stub = object()

for i, test_method in enumerate(tests):
assert_equal(test_method.description, "Test " + str(i))
test_data = [["argument"]]

def test_does_attach_prefix_to_description(self):
tests = self.yield_tests(check, [
["0", 0],
("1", 1),
], described=True, prefix="Test ")
@data_driven_test(test_data)
def func(passed, arg):
assert_is(passed, stub)
assert_equal(arg, "argument")

for i, test_method in enumerate(tests):
assert_equal(test_method.description, "Test " + str(i))
for i, test in enumerate(func(stub)):
test()


class TestAssertMessageContains(Test):
Expand Down

0 comments on commit 3456ede

Please sign in to comment.