From bbcc3e72bd84abfd63a5df2c27a1df2b8ecbbbd1 Mon Sep 17 00:00:00 2001 From: Mikhail Gusarov Date: Sun, 18 Sep 2011 22:35:58 +0200 Subject: [PATCH] Improve unittest compatibility by allowing to add TestCases to Attest collections --- attest/collectors.py | 52 +++++++++++++- attest/tests/collectors.py | 134 ++++++++++++++++++++++++++++++++++++- docs/api/collectors.rst | 1 + setup.py | 3 +- 4 files changed, 187 insertions(+), 3 deletions(-) diff --git a/attest/collectors.py b/attest/collectors.py index c2b7cad..8d2cb2f 100644 --- a/attest/collectors.py +++ b/attest/collectors.py @@ -4,6 +4,8 @@ import inspect import re import sys +import types +import unittest from contextlib import contextmanager from functools import wraps @@ -22,6 +24,44 @@ 'TestBase', ] +class TestCaseProxy(object): + def __init__(self, testcase): + self.testcase = testcase + + def _testcase_debug(self): + """Run the test without collecting errors in a TestResult""" + self.setUp() + try: + getattr(self, self._testMethodName)() + finally: + self.tearDown() + while self._cleanups: + function, args, kwargs = self._cleanups.pop(-1) + function(*args, **kwargs) + self.testcase.debug = types.MethodType(_testcase_debug, self.testcase) + + def __call__(self): + return self.testcase.debug() + + def __eq__(self, rhs): + if not isinstance(rhs, TestCaseProxy): + return False + return self.testcase == rhs.testcase + + +def find_test_case_methods(test_class): + def is_test_method(attrname): + return attrname.startswith('test') \ + and callable(getattr(test_class, attrname)) + return sorted(filter(is_test_method, dir(test_class))) + +def find_test_case_entries(test_class): + method_names = find_test_case_methods(test_class) + if method_names: + for method_name in method_names: + yield TestCaseProxy(test_class(method_name)) + else: + yield TestCaseProxy(test_class()) class Tests(object): """Collection of test functions. @@ -203,7 +243,15 @@ def register(self, tests): """ if inspect.isclass(tests): - self._tests.extend(tests()) + if issubclass(tests, unittest.TestCase): + self.register(find_test_case_entries(tests)) + else: + test = tests() + try: + iterable = iter(test) + except TypeError: + iterable = [test] + self.register(iterable) return tests elif isinstance(tests, basestring): def istests(obj): @@ -215,6 +263,8 @@ def istests(obj): return tests = obj for test in tests: + if isinstance(test, unittest.TestCase): + test = TestCaseProxy(test) if not test in self._tests: self._tests.append(test) diff --git a/attest/tests/collectors.py b/attest/tests/collectors.py index 91b5d5f..1e7f941 100644 --- a/attest/tests/collectors.py +++ b/attest/tests/collectors.py @@ -1,6 +1,7 @@ from __future__ import with_statement from attest import (AbstractReporter, Tests, TestBase, Assert, assert_hook, test, TestFailure) +import unittest class TestReporter(AbstractReporter): @@ -177,7 +178,7 @@ def exclude(): @suite.test -def unittest(): +def unittest_compat(): """Compatibility with Python's unittest package""" signals = set() @@ -238,3 +239,134 @@ def test_something(): assert TestCase.test_something assert TestCase.test_lambda +def unittest_wrap(): + """@Tests().unittest_wrap""" + + col = Tests() + + class UnitTestTestCase(unittest.TestCase): + def runTest(self): + pass + UnitTestTestCase = col.register(UnitTestTestCase) + + assert len(col) == 1 + +@suite.test +def unittest_run_pass(): + """Wrapping passing TestCase""" + + col = Tests() + + class UnitTestTestCase(unittest.TestCase): + def runTest(self): + pass + UnitTestTestCase = col.register(UnitTestTestCase) + + result = TestReporter() + col.run(result) + assert len(result.failed) == 0 + assert len(result.succeeded) == 1 + +@suite.test +def unittest_run_fail_assert(): + """Wrapping TestCase failing with assert""" + + col = Tests() + + class UnitTestTestCase(unittest.TestCase): + def runTest(self): + assert 2 + 2 == 5 + UnitTestTestCase = col.register(UnitTestTestCase) + + result = TestReporter() + col.run(result) + + assert len(result.failed) == 1 + assert len(result.succeeded) == 0 + +@suite.test +def unittest_add_instance(): + """Wrapping TestCase instances""" + + col = Tests() + + class PassingUnitTestTestCase(unittest.TestCase): + def runTest(self): + pass + class FailingUnitTestTestCase(unittest.TestCase): + def runTest(self): + assert 2 + 2 == 5 + + col.register([PassingUnitTestTestCase(), FailingUnitTestTestCase()]) + + result = TestReporter() + col.run(result) + + assert len(result.failed) == 1 + assert len(result.succeeded) == 1 + +@suite.test +def unittest_check_setup_teardown(): + """Checking setUp/tearDown functioning""" + + col = Tests() + + class SetUpTearDownTestCase(unittest.TestCase): + def __init__(self): + self.set_up_invoked = False + self.tear_down_invoked = False + super(SetUpTearDownTestCase, self).__init__() + + def setUp(self): + self.set_up_invoked = True + def runTest(self): + pass + def tearDown(self): + if not self.set_up_invoked: + raise RuntimeError() + self.tear_down_invoked = True + + t = SetUpTearDownTestCase() + col.register([t]) + + result = TestReporter() + col.run(result) + + assert len(result.succeeded) == 1 + assert t.set_up_invoked == True + assert t.tear_down_invoked == True + +@suite.test +def unittest_check_test_functions(): + """Checking test* functions""" + + col = Tests() + + class MultiTestCase(unittest.TestCase): + def setUp(self): + MultiTestCase.set_up_invoked += 1 + def testFailOne(self): + assert False + def testFailTwo(self): + assert False + def testPassOne(self): + assert True + def testPassTwo(self): + assert True + def testPassThree(self): + assert True + def tearDown(self): + MultiTestCase.tear_down_invoked += 1 + + MultiTestCase.set_up_invoked = 0 + MultiTestCase.tear_down_invoked = 0 + + col.register(MultiTestCase) + + result = TestReporter() + col.run(result) + + assert MultiTestCase.set_up_invoked == 5 + assert MultiTestCase.tear_down_invoked == 5 + assert len(result.succeeded) == 3 + assert len(result.failed) == 2 diff --git a/docs/api/collectors.rst b/docs/api/collectors.rst index 6a8083f..771f467 100644 --- a/docs/api/collectors.rst +++ b/docs/api/collectors.rst @@ -16,6 +16,7 @@ and separation. * Lists of lambdas and function references. * :class:`Tests` instances. * Instances of subclasses of :class:`TestBase`. + * Instances of subclasses of :class:`unittest.TestCase`. * Classes are instantiated and returned, allowing :meth:`Tests.register` to be used as a class decorator in Python 2.6 or later. diff --git a/setup.py b/setup.py index ca1abd5..3e3909b 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,8 @@ Attest is a unit testing framework built from the ground up with idiomatic Python in mind. Unlike others, it is not built on top of unittest though it -provides compatibility by creating TestSuites from Attest collections. +provides compatibility by creating TestSuites from Attest collections and adding +TestCases to Attest collections. It has a functional API inspired by `Flask`_ and a class-based API that mimics Python itself. The core avoids complicated assumptions leaving you