diff --git a/.travis.yml b/.travis.yml index fe11b19..f872f70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,25 @@ language: python sudo: false -python: - - 2.6 - - 2.7 - - pypy - - 3.3 - - 3.4 -install: - - "pip install --use-mirrors nose==1.2.1" - - "python setup.py develop" -script: nosetests +env: + - TOXENV=py26-nose + - TOXENV=py26-nose2 + - TOXENV=py26-pytest + - TOXENV=py26-unit + - TOXENV=py26-unit2 + - TOXENV=py27-nose + - TOXENV=py27-nose2 + - TOXENV=py27-pytest + - TOXENV=py27-unit + - TOXENV=py27-unit2 + - TOXENV=py33-nose + - TOXENV=py33-nose2 + - TOXENV=py33-pytest + - TOXENV=py33-unit + - TOXENV=py33-unit2 + - TOXENV=pypy-nose + - TOXENV=pypy-nose2 + - TOXENV=pypy-pytest + - TOXENV=pypy-unit + - TOXENV=pypy-unit2 +install: pip install tox +script: tox diff --git a/CHANGELOG.txt b/CHANGELOG.txt index fb21eef..dc6f7de 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.5.0 (2015-06-09) + * Support for nose2, py.test, unittest, and unittest2 + (nose2 support thanks to @marek-mazur; + https://github.com/wolever/nose-parameterized/pull/26) + 0.4.2 (2015-05-18) * Fix bug with expand + empty arguments (thanks @jikamens; https://github.com/wolever/nose-parameterized/pull/25) diff --git a/README.rst b/README.rst index 7b3a032..2aff314 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,10 @@ -``nose-parameterized`` is a decorator for parameterized testing with ``nose`` -============================================================================= +Parameterized testing with any Python test framework +==================================================== -*Now with 100% less Python 3 incompatibility!* +Parameterized testing in Python sucks. -Nose. It's got test generators. But they kind of suck: - - * They often require a second function - * They make it difficult to separate the data from the test - * They don't work with subclases of ``unittest.TestCase`` - * ``kwargs``? What ``kwargs``? - -But ``nose-parameterized`` fixes that: +``nose-parameterized`` fixes that. For everything. Parameterized testing for +nose, parameterized testing for py.test, parameterized testing for unittest. .. code:: python @@ -39,7 +33,7 @@ But ``nose-parameterized`` fixes that: def test_floor(self, name, input, expected): assert_equal(math.floor(input), expected) -:: +With nose (and nose2):: $ nosetests -v test_math.py test_math.test_pow(2, 2, 4) ... ok @@ -55,6 +49,96 @@ But ``nose-parameterized`` fixes that: OK +As the package name suggests, nose is best supported and will be used for all +further examples. + +With py.test (version 2.0 and above):: + + $ py.test -v test_math.py + ============================== test session starts ============================== + platform darwin -- Python 2.7.2 -- py-1.4.30 -- pytest-2.7.1 + collected 7 items + + test_math.py::test_pow::[0] PASSED + test_math.py::test_pow::[1] PASSED + test_math.py::test_pow::[2] PASSED + test_math.py::test_pow::[3] PASSED + test_math.py::TestMathUnitTest::test_floor_0_negative + test_math.py::TestMathUnitTest::test_floor_1_integer + test_math.py::TestMathUnitTest::test_floor_2_large_fraction + + =========================== 7 passed in 0.10 seconds ============================ + +With unittest (and unittest2):: + + $ python -m unittest -v test_math + test_floor_0_negative (test_math.TestMathUnitTest) ... ok + test_floor_1_integer (test_math.TestMathUnitTest) ... ok + test_floor_2_large_fraction (test_math.TestMathUnitTest) ... ok + + ---------------------------------------------------------------------- + Ran 3 tests in 0.000s + + OK + +(note: because unittest does not support test decorators, only tests created +with ``@parameterized.expand`` will be executed) + +Compatibility +------------- + +`Yes`__. + +__ https://travis-ci.org/wolever/nose-parameterized + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - + - Py2.6 + - Py2.7 + - Py3.3 + - Py3.4 + - PyPy + * - nose + - yes + - yes + - yes + - yes + - yes + * - nose2 + - yes + - yes + - yes + - yes + - yes + * - py.test + - yes + - yes + - yes + - yes + - yes + * - | unittest + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - yes + * - | unittest2 + | (``@parameterized.expand``) + - yes + - yes + - yes + - yes + - yes + +Dependencies +------------ + +(this section left intentionally blank) + Exhaustive Usage Examples -------------------------- @@ -213,3 +297,22 @@ case. It can be used to pass keyword arguments to test cases: ]) def test_int(str_val, expected, base=10): assert_equal(int(str_val, base=base), expected) + + +FAQ +--- + +If all the major testing frameworks are supported, why is it called ``nose-parameterized``? + Originally only nose was supported. But now everything is supported! + +What do you mean when you say "nose is best supported"? + There are small caveates with ``py.test`` and ``unittest``: ``py.test`` + does not show the parameter values (ex, it will show ``test_add[0]`` + instead of ``test_add[1, 2, 3]``), and ``unittest``/``unittest2`` do not + support test generators so ``@parameterized.expand`` must be used. + + +Why not use ``@pytest.mark.parametrize``? + Because spelling is difficult. Also, ``nose-parameterized`` doesn't + require you to repeat argument names, and (using ``param``) it supports + optional keyword arguments. diff --git a/nose_parameterized/parameterized.py b/nose_parameterized/parameterized.py index 5809370..a12f674 100644 --- a/nose_parameterized/parameterized.py +++ b/nose_parameterized/parameterized.py @@ -12,6 +12,8 @@ except ImportError: from unittest import TestCase PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + if PY3: def new_instancemethod(f, *args): @@ -361,8 +363,19 @@ class parameterized(object): @wraps(func) def standalone_func(*a): return func(*(a + p.args), **p.kwargs) - standalone_func.__name__ = name + + # place_as is used by py.test to determine what source file should be + # used for this test. + standalone_func.place_as = func + + # Remove __wrapped__ because py.test will try to look at __wrapped__ + # to determine which parameters should be used with this test case, + # and obviously we don't need it to do any parameterization. + try: + del standalone_func.__wrapped__ + except AttributeError: + pass return standalone_func @classmethod diff --git a/nose_parameterized/test.py b/nose_parameterized/test.py index af4756e..7309d2b 100644 --- a/nose_parameterized/test.py +++ b/nose_parameterized/test.py @@ -6,33 +6,48 @@ from nose.tools import assert_equal from nose.plugins.skip import SkipTest from .parameterized import ( - PY3, parameterized, param, parameterized_argument_value_pairs, short_repr, + PY3, PY2, parameterized, param, parameterized_argument_value_pairs, + short_repr, ) def assert_contains(haystack, needle): if needle not in haystack: raise AssertionError("%r not in %r" %(needle, haystack)) -missing_tests = set([ - "test_naked_function(42, bar=None)", - "test_naked_function('foo0', bar=None)", - "test_naked_function('foo1', bar=None)", - "test_naked_function('foo2', bar=42)", - "test_instance_method(42, bar=None)", - "test_instance_method('foo0', bar=None)", - "test_instance_method('foo1', bar=None)", - "test_instance_method('foo2', bar=42)", - "test_on_TestCase(42, bar=None)", - "test_on_TestCase('foo0', bar=None)", - "test_on_TestCase('foo1', bar=None)", - "test_on_TestCase('foo2', bar=42)", - "test_on_TestCase2_custom_name_42(42, bar=None)", - "test_on_TestCase2_custom_name_foo0('foo0', bar=None)", - "test_on_TestCase2_custom_name_foo1('foo1', bar=None)", - "test_on_TestCase2_custom_name_foo2('foo2', bar=42)", - "test_on_old_style_class('foo')", - "test_on_old_style_class('bar')", -]) +def detect_runner(candidates): + for x in reversed(inspect.stack()): + frame = x[0] + for mod in candidates: + frame_mod = frame.f_globals.get("__name__", "") + if frame_mod == mod or frame_mod.startswith(mod + "."): + return mod + return "" + +runner = detect_runner(["nose", "nose2","unittest", "unittest2"]) +UNITTEST = runner.startswith("unittest") +NOSE2 = (runner == "nose2") + +SKIP_FLAGS = { + "generator": UNITTEST, + # nose2 doesn't run tests on old-style classes under Py2, so don't expect + # these tests to run under nose2. + "py2nose2": (PY2 and NOSE2), +} + +missing_tests = set() + +def expect(skip, tests=None): + if tests is None: + tests = skip + skip = None + if any(SKIP_FLAGS.get(f) for f in (skip or "").split()): + return + missing_tests.update(tests) + + +if not (PY2 and NOSE2): + missing_tests.update([ + ]) test_params = [ (42, ), @@ -41,12 +56,26 @@ test_params = [ param("foo2", bar=42), ] +expect("generator", [ + "test_naked_function('foo0', bar=None)", + "test_naked_function('foo1', bar=None)", + "test_naked_function('foo2', bar=42)", + "test_naked_function(42, bar=None)", +]) + @parameterized(test_params) def test_naked_function(foo, bar=None): missing_tests.remove("test_naked_function(%r, bar=%r)" %(foo, bar)) class TestParameterized(object): + expect("generator", [ + "test_instance_method('foo0', bar=None)", + "test_instance_method('foo1', bar=None)", + "test_instance_method('foo2', bar=42)", + "test_instance_method(42, bar=None)", + ]) + @parameterized(test_params) def test_instance_method(self, foo, bar=None): missing_tests.remove("test_instance_method(%r, bar=%r)" %(foo, bar)) @@ -60,10 +89,24 @@ def custom_naming_func(custom_tag): class TestParamerizedOnTestCase(TestCase): + expect([ + "test_on_TestCase('foo0', bar=None)", + "test_on_TestCase('foo1', bar=None)", + "test_on_TestCase('foo2', bar=42)", + "test_on_TestCase(42, bar=None)", + ]) + @parameterized.expand(test_params) def test_on_TestCase(self, foo, bar=None): missing_tests.remove("test_on_TestCase(%r, bar=%r)" %(foo, bar)) + expect([ + "test_on_TestCase2_custom_name_42(42, bar=None)", + "test_on_TestCase2_custom_name_foo0('foo0', bar=None)", + "test_on_TestCase2_custom_name_foo1('foo1', bar=None)", + "test_on_TestCase2_custom_name_foo2('foo2', bar=42)", + ]) + @parameterized.expand(test_params, testcase_func_name=custom_naming_func("custom")) def test_on_TestCase2(self, foo, bar=None): @@ -140,10 +183,12 @@ def test_warns_when_using_parameterized_with_TestCase(): else: raise AssertionError("Expected exception not raised") -missing_tests.add("test_wrapped_iterable_input()") +expect("generator", [ + "test_wrapped_iterable_input('foo')", +]) @parameterized(lambda: iter(["foo"])) def test_wrapped_iterable_input(foo): - missing_tests.remove("test_wrapped_iterable_input()") + missing_tests.remove("test_wrapped_iterable_input(%r)" %(foo, )) def test_helpful_error_on_non_iterable_input(): try: @@ -155,11 +200,10 @@ def test_helpful_error_on_non_iterable_input(): raise AssertionError("Expected exception not raised") -def teardown_module(): +def tearDownModule(): missing = sorted(list(missing_tests)) assert_equal(missing, []) - def test_old_style_classes(): if PY3: raise SkipTest("Py3 doesn't have old-style classes") @@ -178,6 +222,11 @@ def test_old_style_classes(): class TestOldStyleClass: + expect("py2nose2 generator", [ + "test_on_old_style_class('foo')", + "test_on_old_style_class('bar')", + ]) + @parameterized.expand(["foo", "bar"]) def test_old_style_classes(self, param): missing_tests.remove("test_on_old_style_class(%r)" %(param, )) diff --git a/setup.py b/setup.py index e422fc4..d9344ec 100644 --- a/setup.py +++ b/setup.py @@ -7,17 +7,23 @@ from setuptools import setup, find_packages os.chdir(os.path.dirname(sys.argv[0]) or ".") +try: + long_description = open("README.rst", "U").read() +except IOError: + long_description = "See https://github.com/wolever/nose-parameterized" + setup( name="nose-parameterized", - version="0.4.2", + version="0.5.0", url="https://github.com/wolever/nose-parameterized", author="David Wolever", author_email="david@wolever.net", - description="Decorator for parameterized testing with Nose", + description="Parameterized testing with any Python test framework", classifiers=[ 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'License :: OSI Approved :: BSD License', ], packages=find_packages(), + long_description=long_description, ) diff --git a/tox.ini b/tox.ini index ce36bfb..1bb9054 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,15 @@ [tox] -envlist=py26,py27,py33,pypy +envlist=py{26,27,33,py}-{nose,nose2,pytest,unit,unit2} [testenv] -deps=nose==1.2.1 -commands=nosetests +deps= + nose + nose2: nose2 + pytest: pytest>=2 + unit2: unittest2 +commands= + nose: nosetests + nose2: nose2 + pytest: py.test nose_parameterized/test.py + unit: python -m unittest nose_parameterized.test + unit2: unit2 nose_parameterized.test