Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow index-only test names #81

Merged
merged 3 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 88 additions & 34 deletions ddt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
# DDT is licensed under the MIT License, included in
# https://github.com/datadriventests/ddt/blob/master/LICENSE.md

import codecs
import inspect
import json
import os
import re
import codecs
from enum import Enum, unique
from functools import wraps
from nose.tools import nottest

try:
import yaml
Expand Down Expand Up @@ -38,6 +40,34 @@
trivial_types = (type(None), bool, int, float, str)


@unique
@nottest
class TestNameFormat(Enum):
"""
An enum to configure how ``mk_test_name()`` to compose a test name. Given
the following example:

.. code-block:: python

@data("a", "b")
def testSomething(self, value):
...

if using just ``@ddt`` or together with ``DEFAULT``:
zorchan marked this conversation as resolved.
Show resolved Hide resolved

* ``testSomething_1_a``
* ``testSomething_2_b``

if using ``INDEX_ONLY``:

* ``testSomething_1``
* ``testSomething_2``

"""
DEFAULT = 0
INDEX_ONLY = 1


def is_trivial(value):
if isinstance(value, trivial_types):
return True
Expand Down Expand Up @@ -110,7 +140,7 @@ def wrapper(func):
return wrapper


def mk_test_name(name, value, index=0):
def mk_test_name(name, value, index=0, name_fmt=TestNameFormat.DEFAULT):
"""
Generate a new name for a test case.

Expand All @@ -126,11 +156,14 @@ def mk_test_name(name, value, index=0):

A "trivial" value is a plain scalar, or a tuple or list consisting
only of trivial values.

The test name format is controlled by enum ``TestNameFormat`` as well. See
the enum documentation for further details.
"""

# Add zeros before index to keep order
index = "{0:0{1}}".format(index + 1, index_len)
if not is_trivial(value):
if name_fmt is TestNameFormat.INDEX_ONLY or not is_trivial(value):
return "{0}_{1}".format(name, index)
try:
value = str(value)
Expand Down Expand Up @@ -263,7 +296,7 @@ def _get_test_data_docstring(func, value):
return None


def ddt(cls):
def ddt(arg=None, **kwargs):
"""
Class decorator for subclasses of ``unittest.TestCase``.

Expand All @@ -286,35 +319,56 @@ def ddt(cls):
for each ``test_name`` key create as many methods in the list of values
from the ``data`` key.

Decorating with the keyword argument ``testNameFormat`` can control the
format of the generated test names. For example:

- ``@ddt(testNameFormat=TestNameFormat.DEFAULT)`` will be index and values.

- ``@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)`` will be index only.

- ``@ddt`` is the same as DEFAULT.

"""
for name, func in list(cls.__dict__.items()):
if hasattr(func, DATA_ATTR):
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(name, getattr(v, "__name__", v), i)
test_data_docstring = _get_test_data_docstring(func, v)
if hasattr(func, UNPACK_ATTR):
if isinstance(v, tuple) or isinstance(v, list):
add_test(
cls,
test_name,
test_data_docstring,
func,
*v
)
fmt_test_name = kwargs.get("testNameFormat", TestNameFormat.DEFAULT)

def wrapper(cls):
for name, func in list(cls.__dict__.items()):
if hasattr(func, DATA_ATTR):
for i, v in enumerate(getattr(func, DATA_ATTR)):
test_name = mk_test_name(
name,
getattr(v, "__name__", v),
i,
fmt_test_name
)
test_data_docstring = _get_test_data_docstring(func, v)
if hasattr(func, UNPACK_ATTR):
if isinstance(v, tuple) or isinstance(v, list):
add_test(
cls,
test_name,
test_data_docstring,
func,
*v
)
else:
# unpack dictionary
add_test(
cls,
test_name,
test_data_docstring,
func,
**v
)
else:
# unpack dictionary
add_test(
cls,
test_name,
test_data_docstring,
func,
**v
)
else:
add_test(cls, test_name, test_data_docstring, func, v)
delattr(cls, name)
elif hasattr(func, FILE_ATTR):
file_attr = getattr(func, FILE_ATTR)
process_file_data(cls, name, func, file_attr)
delattr(cls, name)
return cls
add_test(cls, test_name, test_data_docstring, func, v)
delattr(cls, name)
elif hasattr(func, FILE_ATTR):
file_attr = getattr(func, FILE_ATTR)
process_file_data(cls, name, func, file_attr)
delattr(cls, name)
return cls

# ``arg`` is the unittest's test class when decorating with ``@ddt`` while
# it is ``None`` when decorating a test class with ``@ddt(k=v)``.
return wrapper(arg) if inspect.isclass(arg) else wrapper
2 changes: 1 addition & 1 deletion requirements/build.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-r test.txt

nose
Sphinx
sphinxcontrib-programoutput
58 changes: 55 additions & 3 deletions test/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
except ImportError:
import mock

from ddt import ddt, data, file_data
from ddt import ddt, data, file_data, TestNameFormat
from nose.tools import (
assert_true, assert_equal, assert_is_not_none, assert_raises
assert_true, assert_equal, assert_false, assert_is_not_none, assert_raises
)

from test.mycode import has_three_elements
Expand All @@ -32,11 +32,35 @@ def test_something(self, value):
return value


@ddt(testNameFormat=TestNameFormat.DEFAULT)
class DummyTestNameFormatDefault(object):
"""
Dummy class to test the ddt decorator that generates test names using the
default format (index and values).
"""

@data("a", "b", "c", "d")
def test_something(self, value):
return value


@ddt(testNameFormat=TestNameFormat.INDEX_ONLY)
class DummyTestNameFormatIndexOnly(object):
"""
Dummy class to test the ddt decorator that generates test names using only
the index.
"""

@data("a", "b", "c", "d")
def test_something(self, value):
return value


@ddt
class DummyInvalidIdentifier():
"""
Dummy class to test the data decorator receiving values invalid characters
indentifiers
identifiers
"""

@data('32v2 g #Gmw845h$W b53wi.')
Expand Down Expand Up @@ -134,6 +158,34 @@ def test_ddt():
assert_equal(tests, 4)


def test_ddt_format_test_name_index_only():
"""
Test the ``ddt`` class decorator with ``INDEX_ONLY`` test name format
"""
tests = set(filter(_is_test, DummyTestNameFormatIndexOnly.__dict__))
assert_equal(len(tests), 4)

indexes = range(1, 5)
dataSets = ["a", "b", "c", "d"] # @data from DummyTestNameFormatIndexOnly
for i, d in zip(indexes, dataSets):
assert_true("test_something_{}".format(i) in tests)
assert_false("test_something_{}_{}".format(i, d) in tests)


def test_ddt_format_test_name_default():
"""
Test the ``ddt`` class decorator with ``DEFAULT`` test name format
"""
tests = set(filter(_is_test, DummyTestNameFormatDefault.__dict__))
assert_equal(len(tests), 4)

indexes = range(1, 5)
dataSets = ["a", "b", "c", "d"] # @data from DummyTestNameFormatDefault
for i, d in zip(indexes, dataSets):
assert_false("test_something_{}".format(i) in tests)
assert_true("test_something_{}_{}".format(i, d) in tests)


def test_file_data_test_creation():
"""
Test that the ``file_data`` decorator creates two tests
Expand Down