From ec9286056138c1bc3d5a37bc5c99cd8e9406668d Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 16 Nov 2015 19:08:59 +0100 Subject: [PATCH 01/32] Don't use private APIs --- testtools/tests/test_runtest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index 3ae8b13..970a69b 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -9,7 +9,7 @@ from testtools import ( TestCase, TestResult, ) -from testtools.matchers import MatchesException, Is, Raises +from testtools.matchers import HasLength, MatchesException, Is, Raises from testtools.testresult.doubles import ExtendedTestResult from testtools.tests.helpers import FullStackRunTest @@ -68,9 +68,13 @@ class TestRunTest(TestCase): self.assertEqual(['foo'], log) def test__run_prepared_result_does_not_mask_keyboard(self): + tearDownRuns = [] class Case(TestCase): def test(self): raise KeyboardInterrupt("go") + def tearDown(self): + tearDownRuns.append(self) + super(Case, self).tearDown() case = Case('test') run = RunTest(case) run.result = ExtendedTestResult() @@ -79,7 +83,7 @@ class TestRunTest(TestCase): self.assertEqual( [('startTest', case), ('stopTest', case)], run.result._events) # tearDown is still run though! - self.assertEqual(True, getattr(case, '_TestCase__teardown_called')) + self.assertThat(tearDownRuns, HasLength(1)) def test__run_user_calls_onException(self): case = self.make_case() From cc7f66594f6f04dbaf6d207d91cf7da378618070 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 16 Nov 2015 19:09:09 +0100 Subject: [PATCH 02/32] Allow tests to be run twice --- testtools/testcase.py | 2 ++ testtools/tests/test_testcase.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/testtools/testcase.py b/testtools/testcase.py index afb9c6c..c9b3a5f 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -639,6 +639,8 @@ class TestCase(unittest.TestCase): "super(%s, self).tearDown() from your tearDown()." % (sys.modules[self.__class__.__module__].__file__, self.__class__.__name__)) + self.__setup_called = False + self.__teardown_called = False return ret def _get_test_method(self): diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 185b726..6907f0c 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -1265,6 +1265,18 @@ class TestSetupTearDown(TestCase): "...ValueError...File...testtools/tests/test_testcase.py...", ELLIPSIS)) + def test_runTwice(self): + # Tests can be run twice. + class NormalTest(TestCase): + def test_method(self): + pass + test = NormalTest('test_method') + result = unittest.TestResult() + test.run(result) + test.run(result) + self.expectThat(result.errors, HasLength(0)) + self.assertThat(result.testsRun, Equals(2)) + require_py27_minimum = skipIf( sys.version < '2.7', From ebe5109a4ac26719b1c2b8e1553ca53fb258d0ab Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Tue, 17 Nov 2015 16:12:45 +0100 Subject: [PATCH 03/32] Test _run_teardown, rather than tearDown --- testtools/tests/test_runtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testtools/tests/test_runtest.py b/testtools/tests/test_runtest.py index 970a69b..7c5e106 100644 --- a/testtools/tests/test_runtest.py +++ b/testtools/tests/test_runtest.py @@ -72,9 +72,9 @@ class TestRunTest(TestCase): class Case(TestCase): def test(self): raise KeyboardInterrupt("go") - def tearDown(self): + def _run_teardown(self, result): tearDownRuns.append(self) - super(Case, self).tearDown() + return super(Case, self)._run_teardown(result) case = Case('test') run = RunTest(case) run.result = ExtendedTestResult() From df8b8c0c9e3de2c8cff3605e4b5f440e5a368d25 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Wed, 18 Nov 2015 08:02:02 +0100 Subject: [PATCH 04/32] Handle failure in tearDown --- testtools/testcase.py | 25 +++++++++++++------------ testtools/tests/test_testcase.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index c9b3a5f..160351c 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -630,18 +630,19 @@ class TestCase(unittest.TestCase): :raises ValueError: If the base class tearDown is not called, a ValueError is raised. """ - ret = self.tearDown() - if not self.__teardown_called: - raise ValueError( - "In File: %s\n" - "TestCase.tearDown was not called. Have you upcalled all the " - "way up the hierarchy from your tearDown? e.g. Call " - "super(%s, self).tearDown() from your tearDown()." - % (sys.modules[self.__class__.__module__].__file__, - self.__class__.__name__)) - self.__setup_called = False - self.__teardown_called = False - return ret + try: + return self.tearDown() + finally: + if not self.__teardown_called: + raise ValueError( + "In File: %s\n" + "TestCase.tearDown was not called. Have you upcalled all the " + "way up the hierarchy from your tearDown? e.g. Call " + "super(%s, self).tearDown() from your tearDown()." + % (sys.modules[self.__class__.__module__].__file__, + self.__class__.__name__)) + self.__setup_called = False + self.__teardown_called = False def _get_test_method(self): method_name = getattr(self, '_testMethodName') diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 6907f0c..e8aae83 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -29,10 +29,13 @@ from testtools.content import ( TracebackContent, ) from testtools.matchers import ( + AfterPreprocessing, + AllMatch, Annotate, DocTestMatches, Equals, HasLength, + MatchesAll, MatchesException, Raises, ) @@ -1277,6 +1280,33 @@ class TestSetupTearDown(TestCase): self.expectThat(result.errors, HasLength(0)) self.assertThat(result.testsRun, Equals(2)) + def test_runTwiceTearDownFailure(self): + # Tests can be run twice, even if tearDown fails. + class NormalTest(TestCase): + def test_method(self): + pass + def tearDown(self): + super(NormalTest, self).tearDown() + 1/0 + test = NormalTest('test_method') + result = unittest.TestResult() + test.run(result) + # We don't want the errors of the first test to appear in the last + # test. + test.getDetails().clear() + test.run(result) + self.expectThat(result.testsRun, Equals(2)) + # We have code that discourages people from calling setUp() + # explicitly. Here we make sure that this code is not being activated + # when we run a test twice. + def count_tracebacks(exception): + """Number of tracebacks in exception.""" + return str(exception).count('Traceback (most recent call last):') + self.assertThat( + [e[1] for e in result.errors], + AllMatch( + MatchesAll(AfterPreprocessing(count_tracebacks, Equals(1))))) + require_py27_minimum = skipIf( sys.version < '2.7', From 8710d1de5172c37cf8ed40038b073b0d96aa228e Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 27 Nov 2015 18:07:36 +0000 Subject: [PATCH 05/32] Split run twice tests to separate case --- testtools/tests/test_testcase.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index e8aae83..695b1a5 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -1268,6 +1268,15 @@ class TestSetupTearDown(TestCase): "...ValueError...File...testtools/tests/test_testcase.py...", ELLIPSIS)) + +class TestRunTwice(TestCase): + """Can we run the same test case twice?""" + + # XXX: Reviewer, please note that all of the other test cases in this + # module are doing this wrong, saying 'run_test_with' instead of + # 'run_tests_with'. + run_tests_with = FullStackRunTest + def test_runTwice(self): # Tests can be run twice. class NormalTest(TestCase): From 4962652dbf3726a2c30ff62c349ac3d88328a5d8 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 27 Nov 2015 18:55:14 +0000 Subject: [PATCH 06/32] Begin to write scenario-based tests --- testtools/testcase.py | 16 +++++++++++----- testtools/tests/samplecases.py | 31 +++++++++++++++++++++++++++++++ testtools/tests/test_testcase.py | 23 +++++++++++++---------- 3 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 testtools/tests/samplecases.py diff --git a/testtools/testcase.py b/testtools/testcase.py index 160351c..d219175 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -206,11 +206,7 @@ class TestCase(unittest.TestCase): # Generators to ensure unique traceback ids. Maps traceback label to # iterators. self._traceback_id_gens = {} - self.__setup_called = False - self.__teardown_called = False - # __details is lazy-initialized so that a constructed-but-not-run - # TestCase is safe to use with clone_test_with_new_id. - self.__details = None + self._reset() test_method = self._get_test_method() if runTest is None: runTest = getattr( @@ -230,6 +226,15 @@ class TestCase(unittest.TestCase): (Exception, self._report_error), ] + def _reset(self): + """Reset the test case as if it had never been run.""" + self._traceback_id_gens = {} + self.__setup_called = False + self.__teardown_called = False + # __details is lazy-initialized so that a constructed-but-not-run + # TestCase is safe to use with clone_test_with_new_id. + self.__details = None + def __eq__(self, other): eq = getattr(unittest.TestCase, '__eq__', None) if eq is not None and not unittest.TestCase.__eq__(self, other): @@ -596,6 +601,7 @@ class TestCase(unittest.TestCase): result.addUnexpectedSuccess(self, details=self.getDetails()) def run(self, result=None): + self._reset() try: run_test = self.__RunTest( self, self.exception_handlers, last_resort=self._report_error) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py new file mode 100644 index 0000000..4f0f12a --- /dev/null +++ b/testtools/tests/samplecases.py @@ -0,0 +1,31 @@ +# Copyright (c) 2015 testtools developers. See LICENSE for details. + +"""A collection of sample TestCases. + +These are primarily of use in testing the test framework. +""" + +from testtools import TestCase + + +class _NormalTest(TestCase): + + def test_success(self): + pass + + def test_error(self): + 1/0 + + def test_failure(self): + self.fail('arbitrary failure') + + +""" +A list that can be used with testscenarios to test every kind of sample +case that we have. +""" +all_sample_cases_scenarios = [ + ('simple-success-test', {'case': _NormalTest('test_success')}), + ('simple-error-test', {'case': _NormalTest('test_error')}), + ('simple-failure-test', {'case': _NormalTest('test_failure')}), +] diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 695b1a5..c3f8a93 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -54,6 +54,7 @@ from testtools.tests.helpers import ( FullStackRunTest, LoggingResult, ) +from testtools.tests.samplecases import all_sample_cases_scenarios class TestPlaceHolder(TestCase): @@ -1277,19 +1278,21 @@ class TestRunTwice(TestCase): # 'run_tests_with'. run_tests_with = FullStackRunTest + scenarios = all_sample_cases_scenarios + def test_runTwice(self): - # Tests can be run twice. - class NormalTest(TestCase): - def test_method(self): - pass - test = NormalTest('test_method') - result = unittest.TestResult() - test.run(result) - test.run(result) - self.expectThat(result.errors, HasLength(0)) - self.assertThat(result.testsRun, Equals(2)) + # Tests that are intrinsically determistic can be run twice and + # produce exactly the same results each time, without need for + # explicit resetting or reconstruction. + test = self.case + first_result = ExtendedTestResult() + test.run(first_result) + second_result = ExtendedTestResult() + test.run(second_result) + self.assertEqual(first_result._events, second_result._events) def test_runTwiceTearDownFailure(self): + # XXX: jml: you should move this into _samplecases. # Tests can be run twice, even if tearDown fails. class NormalTest(TestCase): def test_method(self): From e4e85321fcd874ac6a2962133b1c0beb0c49c546 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 30 Nov 2015 10:08:18 +0000 Subject: [PATCH 07/32] Treat tear down failure as a scenario --- testtools/tests/samplecases.py | 12 ++++++++++++ testtools/tests/test_testcase.py | 28 ---------------------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 4f0f12a..b19c6e2 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -20,6 +20,17 @@ class _NormalTest(TestCase): self.fail('arbitrary failure') +class _TearDownFails(TestCase): + """Passing test case with failing tearDown after upcall.""" + + def test_success(self): + pass + + def tearDown(self): + super(_TearDownFails, self).tearDown() + 1/0 + + """ A list that can be used with testscenarios to test every kind of sample case that we have. @@ -28,4 +39,5 @@ all_sample_cases_scenarios = [ ('simple-success-test', {'case': _NormalTest('test_success')}), ('simple-error-test', {'case': _NormalTest('test_error')}), ('simple-failure-test', {'case': _NormalTest('test_failure')}), + ('teardown-fails', {'case': _TearDownFails('test_success')}), ] diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index c3f8a93..5f07fd2 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -1291,34 +1291,6 @@ class TestRunTwice(TestCase): test.run(second_result) self.assertEqual(first_result._events, second_result._events) - def test_runTwiceTearDownFailure(self): - # XXX: jml: you should move this into _samplecases. - # Tests can be run twice, even if tearDown fails. - class NormalTest(TestCase): - def test_method(self): - pass - def tearDown(self): - super(NormalTest, self).tearDown() - 1/0 - test = NormalTest('test_method') - result = unittest.TestResult() - test.run(result) - # We don't want the errors of the first test to appear in the last - # test. - test.getDetails().clear() - test.run(result) - self.expectThat(result.testsRun, Equals(2)) - # We have code that discourages people from calling setUp() - # explicitly. Here we make sure that this code is not being activated - # when we run a test twice. - def count_tracebacks(exception): - """Number of tracebacks in exception.""" - return str(exception).count('Traceback (most recent call last):') - self.assertThat( - [e[1] for e in result.errors], - AllMatch( - MatchesAll(AfterPreprocessing(count_tracebacks, Equals(1))))) - require_py27_minimum = skipIf( sys.version < '2.7', From facacbd9bcb19310688a492d87c7d789c1d0e9bc Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 30 Nov 2015 10:22:25 +0000 Subject: [PATCH 08/32] Rename all_sample_cases_scenarios to deterministic_sample_cases_scenarios --- testtools/tests/samplecases.py | 4 ++-- testtools/tests/test_testcase.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index b19c6e2..557d575 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -32,10 +32,10 @@ class _TearDownFails(TestCase): """ -A list that can be used with testscenarios to test every kind of sample +A list that can be used with testscenarios to test every deterministic sample case that we have. """ -all_sample_cases_scenarios = [ +deterministic_sample_cases_scenarios = [ ('simple-success-test', {'case': _NormalTest('test_success')}), ('simple-error-test', {'case': _NormalTest('test_error')}), ('simple-failure-test', {'case': _NormalTest('test_failure')}), diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 5f07fd2..5fbdbbc 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -54,7 +54,7 @@ from testtools.tests.helpers import ( FullStackRunTest, LoggingResult, ) -from testtools.tests.samplecases import all_sample_cases_scenarios +from testtools.tests.samplecases import deterministic_sample_cases_scenarios class TestPlaceHolder(TestCase): @@ -1278,7 +1278,7 @@ class TestRunTwice(TestCase): # 'run_tests_with'. run_tests_with = FullStackRunTest - scenarios = all_sample_cases_scenarios + scenarios = deterministic_sample_cases_scenarios def test_runTwice(self): # Tests that are intrinsically determistic can be run twice and From dfceeab9520fa331b8b4b7a5d412fd1fc8a8263a Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 30 Nov 2015 10:22:54 +0000 Subject: [PATCH 09/32] Simplify cleanup code --- testtools/testcase.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index d219175..394e1d2 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -636,19 +636,16 @@ class TestCase(unittest.TestCase): :raises ValueError: If the base class tearDown is not called, a ValueError is raised. """ - try: - return self.tearDown() - finally: - if not self.__teardown_called: - raise ValueError( - "In File: %s\n" - "TestCase.tearDown was not called. Have you upcalled all the " - "way up the hierarchy from your tearDown? e.g. Call " - "super(%s, self).tearDown() from your tearDown()." - % (sys.modules[self.__class__.__module__].__file__, - self.__class__.__name__)) - self.__setup_called = False - self.__teardown_called = False + ret = self.tearDown() + if not self.__teardown_called: + raise ValueError( + "In File: %s\n" + "TestCase.tearDown was not called. Have you upcalled all the " + "way up the hierarchy from your tearDown? e.g. Call " + "super(%s, self).tearDown() from your tearDown()." + % (sys.modules[self.__class__.__module__].__file__, + self.__class__.__name__)) + return ret def _get_test_method(self): method_name = getattr(self, '_testMethodName') From c88d030f59fe707a889c4d5e7f6c8da9e3f97623 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 30 Nov 2015 10:50:53 +0000 Subject: [PATCH 10/32] Non-deterministic test case --- testtools/tests/samplecases.py | 71 ++++++++++++++++++++++++++++++++ testtools/tests/test_testcase.py | 30 +++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 557d575..556648d 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -6,6 +6,13 @@ These are primarily of use in testing the test framework. """ from testtools import TestCase +from testtools.matchers import ( + AfterPreprocessing, + Contains, + Equals, + MatchesDict, + MatchesListwise, +) class _NormalTest(TestCase): @@ -31,6 +38,61 @@ class _TearDownFails(TestCase): 1/0 +class _SetUpFailsOnGlobalState(TestCase): + """Fail to upcall setUp on first run. Fail to upcall tearDown after. + + This simulates a test that fails to upcall in ``setUp`` if some global + state is broken, and fails to call ``tearDown`` at all. + """ + + first_run = True + + def setUp(self): + if not self.first_run: + return + super(_SetUpFailsOnGlobalState, self).setUp() + + def test_success(self): + pass + + def tearDown(self): + if not self.first_run: + super(_SetUpFailsOnGlobalState, self).tearDown() + self.__class__.first_run = False + + @classmethod + def make_scenario(cls): + case = cls('test_success') + return { + 'case': case, + 'expected_first_result': _test_error_traceback( + case, Contains('TestCase.tearDown was not called')), + 'expected_second_result': _test_error_traceback( + case, Contains('TestCase.setUp was not called')), + } + + +def _test_error_traceback(case, traceback_matcher): + """Match result log of single test that errored out. + + ``traceback_matcher`` is applied to the text of the traceback. + """ + return MatchesListwise([ + Equals(('startTest', case)), + MatchesListwise([ + Equals('addError'), + Equals(case), + MatchesDict({ + 'traceback': AfterPreprocessing( + lambda x: x.as_text(), + traceback_matcher, + ) + }) + ]), + Equals(('stopTest', case)), + ]) + + """ A list that can be used with testscenarios to test every deterministic sample case that we have. @@ -41,3 +103,12 @@ deterministic_sample_cases_scenarios = [ ('simple-failure-test', {'case': _NormalTest('test_failure')}), ('teardown-fails', {'case': _TearDownFails('test_success')}), ] + + +""" +A list that can be used with testscenarios to test every non-deterministic +sample case that we have. +""" +nondeterministic_sample_cases_scenarios = [ + ('setup-fails-global-state', _SetUpFailsOnGlobalState.make_scenario()), +] diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 5fbdbbc..ce16c19 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -54,7 +54,10 @@ from testtools.tests.helpers import ( FullStackRunTest, LoggingResult, ) -from testtools.tests.samplecases import deterministic_sample_cases_scenarios +from testtools.tests.samplecases import ( + deterministic_sample_cases_scenarios, + nondeterministic_sample_cases_scenarios, +) class TestPlaceHolder(TestCase): @@ -1270,7 +1273,7 @@ class TestSetupTearDown(TestCase): ELLIPSIS)) -class TestRunTwice(TestCase): +class TestRunTwiceDeterminstic(TestCase): """Can we run the same test case twice?""" # XXX: Reviewer, please note that all of the other test cases in this @@ -1292,6 +1295,29 @@ class TestRunTwice(TestCase): self.assertEqual(first_result._events, second_result._events) +class TestRunTwiceNondeterministic(TestCase): + """Can we run the same test case twice? + + Separate suite for non-deterministic tests, which require more complicated + assertions and scenarios. + """ + + run_tests_with = FullStackRunTest + + scenarios = nondeterministic_sample_cases_scenarios + + def test_runTwice(self): + test = self.case + first_result = ExtendedTestResult() + test.run(first_result) + second_result = ExtendedTestResult() + test.run(second_result) + self.expectThat( + first_result._events, self.expected_first_result) + self.assertThat( + second_result._events, self.expected_second_result) + + require_py27_minimum = skipIf( sys.version < '2.7', "Requires python 2.7 or greater" From 7936d137c18970d22d037fe579a5f0a6c5c2efe7 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 12:34:21 +0000 Subject: [PATCH 11/32] Easy review comments --- testtools/testcase.py | 4 +++- testtools/tests/samplecases.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index 394e1d2..ba0f8b3 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -201,11 +201,13 @@ class TestCase(unittest.TestCase): """ runTest = kwargs.pop('runTest', None) super(TestCase, self).__init__(*args, **kwargs) + # XXX: jml: Before resubmitting, move these into _reset. We don't + # (yet!) have tests driving the change, but it does seem like the more + # correct thing to do. self._cleanups = [] self._unique_id_gen = itertools.count(1) # Generators to ensure unique traceback ids. Maps traceback label to # iterators. - self._traceback_id_gens = {} self._reset() test_method = self._get_test_method() if runTest is None: diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 556648d..5d911ec 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -42,7 +42,8 @@ class _SetUpFailsOnGlobalState(TestCase): """Fail to upcall setUp on first run. Fail to upcall tearDown after. This simulates a test that fails to upcall in ``setUp`` if some global - state is broken, and fails to call ``tearDown`` at all. + state is broken, and fails to call ``tearDown`` when the global state + breaks but works after that. """ first_run = True From 54136e675b2db7cdf503d9a4629aabfabf504fdd Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 12:51:45 +0000 Subject: [PATCH 12/32] Begin implementing full matrix --- testtools/tests/samplecases.py | 69 +++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 5d911ec..51e2371 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -15,16 +15,59 @@ from testtools.matchers import ( ) -class _NormalTest(TestCase): +class _ConstructedTest(TestCase): + """A test case where all of the stages.""" - def test_success(self): - pass + def __init__(self, test_method_name, set_up=None, test_body=None, + tear_down=None, cleanups=()): + """Construct a ``_ConstructedTest``. - def test_error(self): - 1/0 + All callables are unary callables that receive this test as their + argument. - def test_failure(self): - self.fail('arbitrary failure') + :param str test_method_name: The name of the test method. + :param callable set_up: Implementation of setUp. + :param callable test_body: Implementation of the actual test. + Will be assigned to the test method. + :param callable tear_down: Implementation of tearDown. + :param cleanups: Iterable of callables that will be added as + cleanups. + """ + setattr(self, test_method_name, test_body) + super(_ConstructedTest, self).__init__(test_method_name) + self._set_up = set_up if set_up else _do_nothing + self._test_body = test_body if test_body else _do_nothing + self._tear_down = tear_down if tear_down else _do_nothing + self._cleanups = cleanups + + def setUp(self): + super(_ConstructedTest, self).setUp() + for cleanup in self._cleanups: + self.addCleanup(cleanup, self) + self._set_up(self) + + def test_case(self): + self._test_body(self) + + def tearDown(self): + self._tear_down(self) + super(_ConstructedTest, self).tearDown() + + +def _do_nothing(case): + pass + + +def _success(case): + pass + + +def _error(case): + 1/0 + + +def _failure(case): + case.fail('arbitrary failure') class _TearDownFails(TestCase): @@ -99,9 +142,15 @@ A list that can be used with testscenarios to test every deterministic sample case that we have. """ deterministic_sample_cases_scenarios = [ - ('simple-success-test', {'case': _NormalTest('test_success')}), - ('simple-error-test', {'case': _NormalTest('test_error')}), - ('simple-failure-test', {'case': _NormalTest('test_failure')}), + ('simple-success-test', { + 'case': _ConstructedTest('test_success', test_body=_success) + }), + ('simple-error-test', { + 'case': _ConstructedTest('test_error', test_body=_error) + }), + ('simple-failure-test', { + 'case': _ConstructedTest('test_failure', test_body=_failure) + }), ('teardown-fails', {'case': _TearDownFails('test_success')}), ] From 48cd3fb522f8cfbcc67f9d1cb7f370094dcbe49f Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 12:55:53 +0000 Subject: [PATCH 13/32] Handle more test bodies --- testtools/tests/samplecases.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 51e2371..88d0763 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -63,13 +63,25 @@ def _success(case): def _error(case): - 1/0 + 1/0 # arbitrary non-failure exception def _failure(case): case.fail('arbitrary failure') +def _skip(case): + case.skip('arbitrary skip message') + + +def _expected_failure(case): + case.expectFailure('arbitrary expected failure', _failure, case) + + +def _unexpected_success(case): + case.expectFailure('arbitrary unexpected success', _success) + + class _TearDownFails(TestCase): """Passing test case with failing tearDown after upcall.""" @@ -151,6 +163,15 @@ deterministic_sample_cases_scenarios = [ ('simple-failure-test', { 'case': _ConstructedTest('test_failure', test_body=_failure) }), + ('simple-expected-failure-test', { + 'case': _ConstructedTest('test_failure', test_body=_expected_failure) + }), + ('simple-unexpected-success-test', { + 'case': _ConstructedTest('test_failure', test_body=_unexpected_success) + }), + ('simple-skip-test', { + 'case': _ConstructedTest('test_failure', test_body=_skip) + }), ('teardown-fails', {'case': _TearDownFails('test_success')}), ] From 28bd2dc9efd31bd8cb09d9b2d9ec7b1ddc491964 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 13:37:33 +0000 Subject: [PATCH 14/32] Wrap the constructor, hide the class. Export a function-only interface, ala End of Object Inheritance --- testtools/tests/samplecases.py | 57 ++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 88d0763..1f3ec20 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -15,29 +15,38 @@ from testtools.matchers import ( ) +def make_test_case(test_method_name, set_up=None, test_body=None, + tear_down=None, cleanups=()): + """Make a test case with the given behaviors. + + All callables are unary callables that receive this test as their argument. + + :param str test_method_name: The name of the test method. + :param callable set_up: Implementation of setUp. + :param callable test_body: Implementation of the actual test. Will be + assigned to the test method. + :param callable tear_down: Implementation of tearDown. + :param cleanups: Iterable of callables that will be added as cleanups. + + :return: A ``testtools.TestCase``. + """ + set_up = set_up if set_up else _do_nothing + test_body = test_body if test_body else _do_nothing + tear_down = tear_down if tear_down else _do_nothing + return _ConstructedTest( + test_method_name, set_up, test_body, tear_down, cleanups) + + class _ConstructedTest(TestCase): """A test case where all of the stages.""" - def __init__(self, test_method_name, set_up=None, test_body=None, - tear_down=None, cleanups=()): - """Construct a ``_ConstructedTest``. - - All callables are unary callables that receive this test as their - argument. - - :param str test_method_name: The name of the test method. - :param callable set_up: Implementation of setUp. - :param callable test_body: Implementation of the actual test. - Will be assigned to the test method. - :param callable tear_down: Implementation of tearDown. - :param cleanups: Iterable of callables that will be added as - cleanups. - """ + def __init__(self, test_method_name, set_up, test_body, tear_down, + cleanups): setattr(self, test_method_name, test_body) super(_ConstructedTest, self).__init__(test_method_name) - self._set_up = set_up if set_up else _do_nothing - self._test_body = test_body if test_body else _do_nothing - self._tear_down = tear_down if tear_down else _do_nothing + self._set_up = set_up + self._test_body = test_body + self._tear_down = tear_down self._cleanups = cleanups def setUp(self): @@ -155,22 +164,22 @@ case that we have. """ deterministic_sample_cases_scenarios = [ ('simple-success-test', { - 'case': _ConstructedTest('test_success', test_body=_success) + 'case': make_test_case('test_success', test_body=_success) }), ('simple-error-test', { - 'case': _ConstructedTest('test_error', test_body=_error) + 'case': make_test_case('test_error', test_body=_error) }), ('simple-failure-test', { - 'case': _ConstructedTest('test_failure', test_body=_failure) + 'case': make_test_case('test_failure', test_body=_failure) }), ('simple-expected-failure-test', { - 'case': _ConstructedTest('test_failure', test_body=_expected_failure) + 'case': make_test_case('test_failure', test_body=_expected_failure) }), ('simple-unexpected-success-test', { - 'case': _ConstructedTest('test_failure', test_body=_unexpected_success) + 'case': make_test_case('test_failure', test_body=_unexpected_success) }), ('simple-skip-test', { - 'case': _ConstructedTest('test_failure', test_body=_skip) + 'case': make_test_case('test_failure', test_body=_skip) }), ('teardown-fails', {'case': _TearDownFails('test_success')}), ] From 8348d35a80b429ceef063996f22c9401122bed1a Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 13:54:33 +0000 Subject: [PATCH 15/32] Start to use generated behaviors --- testtools/tests/samplecases.py | 59 ++++++++++++++++++++++---------- testtools/tests/test_testcase.py | 28 +++++++++++++-- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 1f3ec20..c7cb7f0 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -91,6 +91,38 @@ def _unexpected_success(case): case.expectFailure('arbitrary unexpected success', _success) +behaviors = [ + ('success', _success), + ('fail', _failure), + ('error', _error), + ('skip', _skip), + ('xfail', _expected_failure), + ('uxsuccess', _unexpected_success), +] + + +def _make_behavior_scenarios(stage): + """Given a test stage, iterate over behavior scenarios for that stage. + + e.g. + >>> list(_make_behavior_scenarios('setUp')) + [('setUp=success', {'setUp_behavior': }), + ('setUp=fail', {'setUp_behavior': }), + ('setUp=error', {'setUp_behavior': }), + ('setUp=skip', {'setUp_behavior': }), + ('setUp=xfail', {'setUp_behavior': ), + ('setUp=uxsuccess', + {'setUp_behavior': })] + + Ordering is not consistent. + """ + return ( + ('%s=%s' % (stage, behavior), + {'%s_behavior' % (stage,): function}) + for (behavior, function) in behaviors + ) + + class _TearDownFails(TestCase): """Passing test case with failing tearDown after upcall.""" @@ -162,25 +194,14 @@ def _test_error_traceback(case, traceback_matcher): A list that can be used with testscenarios to test every deterministic sample case that we have. """ -deterministic_sample_cases_scenarios = [ - ('simple-success-test', { - 'case': make_test_case('test_success', test_body=_success) - }), - ('simple-error-test', { - 'case': make_test_case('test_error', test_body=_error) - }), - ('simple-failure-test', { - 'case': make_test_case('test_failure', test_body=_failure) - }), - ('simple-expected-failure-test', { - 'case': make_test_case('test_failure', test_body=_expected_failure) - }), - ('simple-unexpected-success-test', { - 'case': make_test_case('test_failure', test_body=_unexpected_success) - }), - ('simple-skip-test', { - 'case': make_test_case('test_failure', test_body=_skip) - }), +deterministic_sample_cases_scenarios = _make_behavior_scenarios('body') + + +""" +A list that can be used with testscenarios to test deterministic cases +that we cannot easily construct from our test behavior matrix. +""" +special_deterministic_sample_cases_scenarios = [ ('teardown-fails', {'case': _TearDownFails('test_success')}), ] diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index ce16c19..7f5bfdf 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -29,13 +29,10 @@ from testtools.content import ( TracebackContent, ) from testtools.matchers import ( - AfterPreprocessing, - AllMatch, Annotate, DocTestMatches, Equals, HasLength, - MatchesAll, MatchesException, Raises, ) @@ -56,7 +53,9 @@ from testtools.tests.helpers import ( ) from testtools.tests.samplecases import ( deterministic_sample_cases_scenarios, + make_test_case, nondeterministic_sample_cases_scenarios, + special_deterministic_sample_cases_scenarios, ) @@ -1283,6 +1282,29 @@ class TestRunTwiceDeterminstic(TestCase): scenarios = deterministic_sample_cases_scenarios + def test_runTwice(self): + # Tests that are intrinsically determistic can be run twice and + # produce exactly the same results each time, without need for + # explicit resetting or reconstruction. + + # XXX: jml: before resubmitting. Although make_test_case should be + # exported, it's a bit wrong to be relying on internal details of the + # scenario generation here. + test = make_test_case('test_arbitrary', test_body=self.body_behavior) + first_result = ExtendedTestResult() + test.run(first_result) + second_result = ExtendedTestResult() + test.run(second_result) + self.assertEqual(first_result._events, second_result._events) + + +class TestRunTwiceDeterminsticSpecial(TestCase): + """Can we run the same test case twice?""" + + run_tests_with = FullStackRunTest + + scenarios = special_deterministic_sample_cases_scenarios + def test_runTwice(self): # Tests that are intrinsically determistic can be run twice and # produce exactly the same results each time, without need for From ecd6b95f77131a0bc43f986a79b9b97927051726 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 15:53:57 +0000 Subject: [PATCH 16/32] Encapsulate construction --- testtools/tests/samplecases.py | 5 +++++ testtools/tests/test_testcase.py | 8 ++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index c7cb7f0..423c7bc 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -123,6 +123,11 @@ def _make_behavior_scenarios(stage): ) +def make_case_for_behavior_scenario(case): + """Given a test with a behavior scenario installed, make a TestCase.""" + return make_test_case(case.getUniqueString(), test_body=case.body_behavior) + + class _TearDownFails(TestCase): """Passing test case with failing tearDown after upcall.""" diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 7f5bfdf..4634e06 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -53,7 +53,7 @@ from testtools.tests.helpers import ( ) from testtools.tests.samplecases import ( deterministic_sample_cases_scenarios, - make_test_case, + make_case_for_behavior_scenario, nondeterministic_sample_cases_scenarios, special_deterministic_sample_cases_scenarios, ) @@ -1286,11 +1286,7 @@ class TestRunTwiceDeterminstic(TestCase): # Tests that are intrinsically determistic can be run twice and # produce exactly the same results each time, without need for # explicit resetting or reconstruction. - - # XXX: jml: before resubmitting. Although make_test_case should be - # exported, it's a bit wrong to be relying on internal details of the - # scenario generation here. - test = make_test_case('test_arbitrary', test_body=self.body_behavior) + test = make_case_for_behavior_scenario(self) first_result = ExtendedTestResult() test.run(first_result) second_result = ExtendedTestResult() From 70770264fa103916dce33a5ad11410611e23eba6 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 17:56:19 +0000 Subject: [PATCH 17/32] Complete suite of tests --- testtools/tests/samplecases.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 423c7bc..2120980 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -5,6 +5,8 @@ These are primarily of use in testing the test framework. """ +from testscenarios import multiply_scenarios + from testtools import TestCase from testtools.matchers import ( AfterPreprocessing, @@ -47,11 +49,11 @@ class _ConstructedTest(TestCase): self._set_up = set_up self._test_body = test_body self._tear_down = tear_down - self._cleanups = cleanups + self._test_cleanups = cleanups def setUp(self): super(_ConstructedTest, self).setUp() - for cleanup in self._cleanups: + for cleanup in self._test_cleanups: self.addCleanup(cleanup, self) self._set_up(self) @@ -125,7 +127,13 @@ def _make_behavior_scenarios(stage): def make_case_for_behavior_scenario(case): """Given a test with a behavior scenario installed, make a TestCase.""" - return make_test_case(case.getUniqueString(), test_body=case.body_behavior) + return make_test_case( + case.getUniqueString(), + set_up=case.set_up_behavior, + test_body=case.body_behavior, + tear_down=case.tear_down_behavior, + cleanups=(case.cleanup_behavior,), + ) class _TearDownFails(TestCase): @@ -199,7 +207,12 @@ def _test_error_traceback(case, traceback_matcher): A list that can be used with testscenarios to test every deterministic sample case that we have. """ -deterministic_sample_cases_scenarios = _make_behavior_scenarios('body') +deterministic_sample_cases_scenarios = multiply_scenarios( + _make_behavior_scenarios('set_up'), + _make_behavior_scenarios('body'), + _make_behavior_scenarios('tear_down'), + _make_behavior_scenarios('cleanup'), +) """ From d5db0115c33f8e22eedb750b9163e1bd817e13df Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 18:10:37 +0000 Subject: [PATCH 18/32] Allow pre setUp & post tearDown to be set --- testtools/tests/samplecases.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 2120980..5e4d063 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -18,7 +18,8 @@ from testtools.matchers import ( def make_test_case(test_method_name, set_up=None, test_body=None, - tear_down=None, cleanups=()): + tear_down=None, cleanups=(), pre_set_up=None, + post_tear_down=None): """Make a test case with the given behaviors. All callables are unary callables that receive this test as their argument. @@ -29,29 +30,38 @@ def make_test_case(test_method_name, set_up=None, test_body=None, assigned to the test method. :param callable tear_down: Implementation of tearDown. :param cleanups: Iterable of callables that will be added as cleanups. + :param callable pre_set_up: Called before the upcall to setUp(). + :param callable post_tear_down: Called after the upcall to tearDown(). :return: A ``testtools.TestCase``. """ set_up = set_up if set_up else _do_nothing test_body = test_body if test_body else _do_nothing tear_down = tear_down if tear_down else _do_nothing + pre_set_up = pre_set_up if pre_set_up else _do_nothing + post_tear_down = post_tear_down if post_tear_down else _do_nothing return _ConstructedTest( - test_method_name, set_up, test_body, tear_down, cleanups) + test_method_name, set_up, test_body, tear_down, cleanups, + pre_set_up, post_tear_down, + ) class _ConstructedTest(TestCase): """A test case where all of the stages.""" def __init__(self, test_method_name, set_up, test_body, tear_down, - cleanups): + cleanups, pre_set_up, post_tear_down): setattr(self, test_method_name, test_body) super(_ConstructedTest, self).__init__(test_method_name) self._set_up = set_up self._test_body = test_body self._tear_down = tear_down self._test_cleanups = cleanups + self._pre_set_up = pre_set_up + self._post_tear_down = post_tear_down def setUp(self): + self._pre_set_up(self) super(_ConstructedTest, self).setUp() for cleanup in self._test_cleanups: self.addCleanup(cleanup, self) @@ -63,6 +73,7 @@ class _ConstructedTest(TestCase): def tearDown(self): self._tear_down(self) super(_ConstructedTest, self).tearDown() + self._post_tear_down(self) def _do_nothing(case): From bace52fcebe1a142d3d23895420ac9044abe398b Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 18:10:57 +0000 Subject: [PATCH 19/32] Spell setUp consistently --- testtools/tests/samplecases.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 5e4d063..75496b7 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -118,14 +118,14 @@ def _make_behavior_scenarios(stage): """Given a test stage, iterate over behavior scenarios for that stage. e.g. - >>> list(_make_behavior_scenarios('setUp')) - [('setUp=success', {'setUp_behavior': }), - ('setUp=fail', {'setUp_behavior': }), - ('setUp=error', {'setUp_behavior': }), - ('setUp=skip', {'setUp_behavior': }), - ('setUp=xfail', {'setUp_behavior': ), - ('setUp=uxsuccess', - {'setUp_behavior': })] + >>> list(_make_behavior_scenarios('set_up')) + [('set_up=success', {'set_up_behavior': }), + ('set_up=fail', {'set_up_behavior': }), + ('set_up=error', {'set_up_behavior': }), + ('set_up=skip', {'set_up_behavior': }), + ('set_up=xfail', {'set_up_behavior': ), + ('set_up=uxsuccess', + {'set_up_behavior': })] Ordering is not consistent. """ From 03061713d44cd13a10aa0ebe4ba0a45a3a6b58e5 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 18:12:35 +0000 Subject: [PATCH 20/32] Get pre_set_up & post_tear_down from scenario --- testtools/tests/samplecases.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index 75496b7..d53b7ab 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -138,12 +138,16 @@ def _make_behavior_scenarios(stage): def make_case_for_behavior_scenario(case): """Given a test with a behavior scenario installed, make a TestCase.""" + cleanup_behavior = getattr(case, 'cleanup_behavior', None) + cleanups = [cleanup_behavior] if cleanup_behavior else [] return make_test_case( case.getUniqueString(), - set_up=case.set_up_behavior, - test_body=case.body_behavior, - tear_down=case.tear_down_behavior, - cleanups=(case.cleanup_behavior,), + set_up=getattr(case, 'set_up_behavior', _do_nothing), + test_body=getattr(case, 'body_behavior', _do_nothing), + tear_down=getattr(case, 'tear_down_behavior', _do_nothing), + cleanups=cleanups, + pre_set_up=getattr(case, 'pre_set_up_behavior', _do_nothing), + post_tear_down=getattr(case, 'post_tear_down_behavior', _do_nothing), ) From d841a704866228faa6ed6aa06190ba88750eb5eb Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 18:21:16 +0000 Subject: [PATCH 21/32] No need for special case --- testtools/tests/samplecases.py | 24 ++++-------------------- testtools/tests/test_testcase.py | 22 +--------------------- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index d53b7ab..ce22ffa 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -151,17 +151,6 @@ def make_case_for_behavior_scenario(case): ) -class _TearDownFails(TestCase): - """Passing test case with failing tearDown after upcall.""" - - def test_success(self): - pass - - def tearDown(self): - super(_TearDownFails, self).tearDown() - 1/0 - - class _SetUpFailsOnGlobalState(TestCase): """Fail to upcall setUp on first run. Fail to upcall tearDown after. @@ -227,15 +216,10 @@ deterministic_sample_cases_scenarios = multiply_scenarios( _make_behavior_scenarios('body'), _make_behavior_scenarios('tear_down'), _make_behavior_scenarios('cleanup'), -) - - -""" -A list that can be used with testscenarios to test deterministic cases -that we cannot easily construct from our test behavior matrix. -""" -special_deterministic_sample_cases_scenarios = [ - ('teardown-fails', {'case': _TearDownFails('test_success')}), +) + [ + ('tear_down_fails_after_upcall', { + 'post_tear_down_behavior': _error, + }), ] diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 4634e06..306b22b 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2015 testtools developers. See LICENSE for details. """Tests for extensions to the base test library.""" @@ -55,7 +55,6 @@ from testtools.tests.samplecases import ( deterministic_sample_cases_scenarios, make_case_for_behavior_scenario, nondeterministic_sample_cases_scenarios, - special_deterministic_sample_cases_scenarios, ) @@ -1294,25 +1293,6 @@ class TestRunTwiceDeterminstic(TestCase): self.assertEqual(first_result._events, second_result._events) -class TestRunTwiceDeterminsticSpecial(TestCase): - """Can we run the same test case twice?""" - - run_tests_with = FullStackRunTest - - scenarios = special_deterministic_sample_cases_scenarios - - def test_runTwice(self): - # Tests that are intrinsically determistic can be run twice and - # produce exactly the same results each time, without need for - # explicit resetting or reconstruction. - test = self.case - first_result = ExtendedTestResult() - test.run(first_result) - second_result = ExtendedTestResult() - test.run(second_result) - self.assertEqual(first_result._events, second_result._events) - - class TestRunTwiceNondeterministic(TestCase): """Can we run the same test case twice? From 8393880f3f0a10d568323b15106fc714829af3a6 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 18:25:44 +0000 Subject: [PATCH 22/32] Clean up comments --- testtools/testcase.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index ba0f8b3..f064494 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -201,13 +201,8 @@ class TestCase(unittest.TestCase): """ runTest = kwargs.pop('runTest', None) super(TestCase, self).__init__(*args, **kwargs) - # XXX: jml: Before resubmitting, move these into _reset. We don't - # (yet!) have tests driving the change, but it does seem like the more - # correct thing to do. self._cleanups = [] self._unique_id_gen = itertools.count(1) - # Generators to ensure unique traceback ids. Maps traceback label to - # iterators. self._reset() test_method = self._get_test_method() if runTest is None: @@ -230,6 +225,8 @@ class TestCase(unittest.TestCase): def _reset(self): """Reset the test case as if it had never been run.""" + # Generators to ensure unique traceback ids. Maps traceback label to + # iterators. self._traceback_id_gens = {} self.__setup_called = False self.__teardown_called = False From db7e382f815d218b529b97e0118a003375d46ba9 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Fri, 4 Dec 2015 18:27:13 +0000 Subject: [PATCH 23/32] Move unique_id_gen to reset --- testtools/testcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index f064494..278239b 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -202,7 +202,6 @@ class TestCase(unittest.TestCase): runTest = kwargs.pop('runTest', None) super(TestCase, self).__init__(*args, **kwargs) self._cleanups = [] - self._unique_id_gen = itertools.count(1) self._reset() test_method = self._get_test_method() if runTest is None: @@ -225,6 +224,7 @@ class TestCase(unittest.TestCase): def _reset(self): """Reset the test case as if it had never been run.""" + self._unique_id_gen = itertools.count(1) # Generators to ensure unique traceback ids. Maps traceback label to # iterators. self._traceback_id_gens = {} From 2acf99ba337b298e56b173a0a1dbed0a87e72c56 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 10:48:28 +0000 Subject: [PATCH 24/32] Move useful matchers to helper module --- testtools/tests/helpers.py | 45 ++++++++++++++++++++++++ testtools/tests/test_deferredruntest.py | 46 +++---------------------- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/testtools/tests/helpers.py b/testtools/tests/helpers.py index f766da3..647e374 100644 --- a/testtools/tests/helpers.py +++ b/testtools/tests/helpers.py @@ -12,6 +12,12 @@ from extras import safe_hasattr from testtools import TestResult from testtools.content import StackLinesContent +from testtools.matchers import ( + AfterPreprocessing, + Equals, + MatchesDict, + MatchesListwise, +) from testtools import runtest @@ -106,3 +112,42 @@ class FullStackRunTest(runtest.RunTest): return run_with_stack_hidden( False, super(FullStackRunTest, self)._run_user, fn, *args, **kwargs) + + +class MatchesEvents(object): + """Match a list of test result events. + + Specify events as a data structure. Ordinary Python objects within this + structure will be compared exactly, but you can also use matchers at any + point. + """ + + def __init__(self, *expected): + self._expected = expected + + def _make_matcher(self, obj): + # This isn't very safe for general use, but is good enough to make + # some tests in this module more readable. + if hasattr(obj, 'match'): + return obj + elif isinstance(obj, tuple) or isinstance(obj, list): + return MatchesListwise( + [self._make_matcher(item) for item in obj]) + elif isinstance(obj, dict): + return MatchesDict(dict( + (key, self._make_matcher(value)) + for key, value in obj.items())) + else: + return Equals(obj) + + def match(self, observed): + matcher = self._make_matcher(self._expected) + return matcher.match(observed) + + +class AsText(AfterPreprocessing): + """Match the text of a Content instance.""" + + def __init__(self, matcher, annotate=True): + super(AsText, self).__init__( + lambda log: log.as_text(), matcher, annotate=annotate) diff --git a/testtools/tests/test_deferredruntest.py b/testtools/tests/test_deferredruntest.py index 2eacd3b..dd2de69 100644 --- a/testtools/tests/test_deferredruntest.py +++ b/testtools/tests/test_deferredruntest.py @@ -13,20 +13,21 @@ from testtools import ( TestResult, ) from testtools.matchers import ( - AfterPreprocessing, ContainsAll, EndsWith, Equals, Is, KeysEqual, - MatchesDict, MatchesException, - MatchesListwise, Not, Raises, ) from testtools.runtest import RunTest from testtools.testresult.doubles import ExtendedTestResult +from testtools.tests.helpers import ( + AsText, + MatchesEvents, +) from testtools.tests.test_spinner import NeedsTwistedTestCase assert_fails_with = try_import('testtools.deferredruntest.assert_fails_with') @@ -43,45 +44,6 @@ log = try_import('twisted.python.log') DelayedCall = try_import('twisted.internet.base.DelayedCall') -class MatchesEvents(object): - """Match a list of test result events. - - Specify events as a data structure. Ordinary Python objects within this - structure will be compared exactly, but you can also use matchers at any - point. - """ - - def __init__(self, *expected): - self._expected = expected - - def _make_matcher(self, obj): - # This isn't very safe for general use, but is good enough to make - # some tests in this module more readable. - if hasattr(obj, 'match'): - return obj - elif isinstance(obj, tuple) or isinstance(obj, list): - return MatchesListwise( - [self._make_matcher(item) for item in obj]) - elif isinstance(obj, dict): - return MatchesDict(dict( - (key, self._make_matcher(value)) - for key, value in obj.items())) - else: - return Equals(obj) - - def match(self, observed): - matcher = self._make_matcher(self._expected) - return matcher.match(observed) - - -class AsText(AfterPreprocessing): - """Match the text of a Content instance.""" - - def __init__(self, matcher, annotate=True): - super(AsText, self).__init__( - lambda log: log.as_text(), matcher, annotate=annotate) - - class X(object): """Tests that we run as part of our tests, nested to avoid discovery.""" From 5c258ff81e2d86b00c78f1fe12b7ecc9bd15db9a Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 10:48:54 +0000 Subject: [PATCH 25/32] Use method for value, to avoid `self` weirdness --- testtools/tests/samplecases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index ce22ffa..b6646cb 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -51,7 +51,7 @@ class _ConstructedTest(TestCase): def __init__(self, test_method_name, set_up, test_body, tear_down, cleanups, pre_set_up, post_tear_down): - setattr(self, test_method_name, test_body) + setattr(self, test_method_name, self.test_case) super(_ConstructedTest, self).__init__(test_method_name) self._set_up = set_up self._test_body = test_body From e9ceb70787dd8874e3a5c0feac72eb6bf89f5596 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 11:00:50 +0000 Subject: [PATCH 26/32] Rewrite addCleanup tests in terms of make_test_case --- testtools/tests/test_testcase.py | 278 +++++++++++++++++++------------ 1 file changed, 168 insertions(+), 110 deletions(-) diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 306b22b..73ba715 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -30,6 +30,7 @@ from testtools.content import ( ) from testtools.matchers import ( Annotate, + ContainsAll, DocTestMatches, Equals, HasLength, @@ -48,12 +49,15 @@ from testtools.testresult.doubles import ( ) from testtools.tests.helpers import ( an_exc_info, + AsText, FullStackRunTest, LoggingResult, + MatchesEvents, ) from testtools.tests.samplecases import ( deterministic_sample_cases_scenarios, make_case_for_behavior_scenario, + make_test_case, nondeterministic_sample_cases_scenarios, ) @@ -743,76 +747,42 @@ class TestAssertions(TestCase): class TestAddCleanup(TestCase): """Tests for TestCase.addCleanup.""" - run_test_with = FullStackRunTest + run_tests_with = FullStackRunTest - class LoggingTest(TestCase): - """A test that logs calls to setUp, runTest and tearDown.""" - - def setUp(self): - TestCase.setUp(self) - self._calls = ['setUp'] - - def brokenSetUp(self): - # A tearDown that deliberately fails. - self._calls = ['brokenSetUp'] - raise RuntimeError('Deliberate Failure') - - def runTest(self): - self._calls.append('runTest') - - def brokenTest(self): - raise RuntimeError('Deliberate broken test') - - def tearDown(self): - self._calls.append('tearDown') - TestCase.tearDown(self) - - def setUp(self): - TestCase.setUp(self) - self._result_calls = [] - self.test = TestAddCleanup.LoggingTest('runTest') - self.logging_result = LoggingResult(self._result_calls) - - def assertErrorLogEqual(self, messages): - self.assertEqual(messages, [call[0] for call in self._result_calls]) - - def assertTestLogEqual(self, messages): - """Assert that the call log equals 'messages'.""" - case = self._result_calls[0][1] - self.assertEqual(messages, case._calls) - - def logAppender(self, message): - """A cleanup that appends 'message' to the tests log. - - Cleanups are callables that are added to a test by addCleanup. To - verify that our cleanups run in the right order, we add strings to a - list that acts as a log. This method returns a cleanup that will add - the given message to that log when run. - """ - self.test._calls.append(message) - - def test_fixture(self): - # A normal run of self.test logs 'setUp', 'runTest' and 'tearDown'. - # This test doesn't test addCleanup itself, it just sanity checks the - # fixture. - self.test.run(self.logging_result) - self.assertTestLogEqual(['setUp', 'runTest', 'tearDown']) - - def test_cleanup_run_before_tearDown(self): - # Cleanup functions added with 'addCleanup' are called before tearDown + def test_cleanup_run_after_tearDown(self): + # Cleanup functions added with 'addCleanup' are called after tearDown # runs. - self.test.addCleanup(self.logAppender, 'cleanup') - self.test.run(self.logging_result) - self.assertTestLogEqual(['setUp', 'runTest', 'tearDown', 'cleanup']) + log = [] + test = make_test_case( + self.getUniqueString(), + set_up=lambda _: log.append('setUp'), + test_body=lambda _: log.append('runTest'), + tear_down=lambda _: log.append('tearDown'), + cleanups=[lambda _: log.append('cleanup')], + ) + test.run() + self.assertThat( + log, Equals(['setUp', 'runTest', 'tearDown', 'cleanup'])) def test_add_cleanup_called_if_setUp_fails(self): # Cleanup functions added with 'addCleanup' are called even if setUp # fails. Note that tearDown has a different behavior: it is only # called when setUp succeeds. - self.test.setUp = self.test.brokenSetUp - self.test.addCleanup(self.logAppender, 'cleanup') - self.test.run(self.logging_result) - self.assertTestLogEqual(['brokenSetUp', 'cleanup']) + log = [] + + def broken_set_up(ignored): + log.append('brokenSetUp') + raise RuntimeError('Deliberate broken setUp') + + test = make_test_case( + self.getUniqueString(), + set_up=broken_set_up, + test_body=lambda _: log.append('runTest'), + tear_down=lambda _: log.append('tearDown'), + cleanups=[lambda _: log.append('cleanup')], + ) + test.run() + self.assertThat(log, Equals(['brokenSetUp', 'cleanup'])) def test_addCleanup_called_in_reverse_order(self): # Cleanup functions added with 'addCleanup' are called in reverse @@ -826,46 +796,83 @@ class TestAddCleanup(TestCase): # # When this happens, we generally want to clean up the second resource # before the first one, since the second depends on the first. - self.test.addCleanup(self.logAppender, 'first') - self.test.addCleanup(self.logAppender, 'second') - self.test.run(self.logging_result) - self.assertTestLogEqual( - ['setUp', 'runTest', 'tearDown', 'second', 'first']) + log = [] + test = make_test_case( + self.getUniqueString(), + set_up=lambda _: log.append('setUp'), + test_body=lambda _: log.append('runTest'), + tear_down=lambda _: log.append('tearDown'), + cleanups=[ + lambda _: log.append('first'), + lambda _: log.append('second'), + ], + ) + test.run() + self.assertThat( + log, Equals(['setUp', 'runTest', 'tearDown', 'second', 'first'])) - def test_tearDown_runs_after_cleanup_failure(self): + def test_tearDown_runs_on_cleanup_failure(self): # tearDown runs even if a cleanup function fails. - self.test.addCleanup(lambda: 1/0) - self.test.run(self.logging_result) - self.assertTestLogEqual(['setUp', 'runTest', 'tearDown']) + log = [] + test = make_test_case( + self.getUniqueString(), + set_up=lambda _: log.append('setUp'), + test_body=lambda _: log.append('runTest'), + tear_down=lambda _: log.append('tearDown'), + cleanups=[lambda _: 1/0], + ) + test.run() + self.assertThat(log, Equals(['setUp', 'runTest', 'tearDown'])) def test_cleanups_continue_running_after_error(self): # All cleanups are always run, even if one or two of them fail. - self.test.addCleanup(self.logAppender, 'first') - self.test.addCleanup(lambda: 1/0) - self.test.addCleanup(self.logAppender, 'second') - self.test.run(self.logging_result) - self.assertTestLogEqual( - ['setUp', 'runTest', 'tearDown', 'second', 'first']) + log = [] + test = make_test_case( + self.getUniqueString(), + set_up=lambda _: log.append('setUp'), + test_body=lambda _: log.append('runTest'), + tear_down=lambda _: log.append('tearDown'), + cleanups=[ + lambda _: log.append('first'), + lambda _: 1/0, + lambda _: log.append('second'), + ], + ) + test.run() + self.assertThat( + log, Equals(['setUp', 'runTest', 'tearDown', 'second', 'first'])) def test_error_in_cleanups_are_captured(self): # If a cleanup raises an error, we want to record it and fail the the # test, even though we go on to run other cleanups. - self.test.addCleanup(lambda: 1/0) - self.test.run(self.logging_result) - self.assertErrorLogEqual(['startTest', 'addError', 'stopTest']) + test = make_test_case(self.getUniqueString(), cleanups=[lambda _: 1/0]) + log = [] + test.run(ExtendedTestResult(log)) + self.assertThat( + log, MatchesEvents( + ('startTest', test), + ('addError', test, { + 'traceback': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + }), + ('stopTest', test), + ) + ) def test_keyboard_interrupt_not_caught(self): # If a cleanup raises KeyboardInterrupt, it gets reraised. - def raiseKeyboardInterrupt(): + def raise_keyboard_interrupt(ignored): raise KeyboardInterrupt() - self.test.addCleanup(raiseKeyboardInterrupt) - self.assertThat(lambda:self.test.run(self.logging_result), - Raises(MatchesException(KeyboardInterrupt))) + test = make_test_case( + self.getUniqueString(), cleanups=[raise_keyboard_interrupt]) + self.assertThat(test.run, Raises(MatchesException(KeyboardInterrupt))) def test_all_errors_from_MultipleExceptions_reported(self): # When a MultipleExceptions exception is caught, all the errors are # reported. - def raiseMany(): + def raise_many(ignored): try: 1/0 except Exception: @@ -875,37 +882,88 @@ class TestAddCleanup(TestCase): except Exception: exc_info2 = sys.exc_info() raise MultipleExceptions(exc_info1, exc_info2) - self.test.addCleanup(raiseMany) - self.logging_result = ExtendedTestResult() - self.test.run(self.logging_result) - self.assertEqual(['startTest', 'addError', 'stopTest'], - [event[0] for event in self.logging_result._events]) - self.assertEqual(set(['traceback', 'traceback-1']), - set(self.logging_result._events[1][2].keys())) + + test = make_test_case(self.getUniqueString(), cleanups=[raise_many]) + log = [] + test.run(ExtendedTestResult(log)) + self.assertThat( + log, MatchesEvents( + ('startTest', test), + ('addError', test, { + 'traceback': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + 'traceback-1': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + }), + ('stopTest', test), + ) + ) def test_multipleCleanupErrorsReported(self): # Errors from all failing cleanups are reported as separate backtraces. - self.test.addCleanup(lambda: 1/0) - self.test.addCleanup(lambda: 1/0) - self.logging_result = ExtendedTestResult() - self.test.run(self.logging_result) - self.assertEqual(['startTest', 'addError', 'stopTest'], - [event[0] for event in self.logging_result._events]) - self.assertEqual(set(['traceback', 'traceback-1']), - set(self.logging_result._events[1][2].keys())) + test = make_test_case(self.getUniqueString(), cleanups=[ + lambda _: 1/0, + lambda _: 1/0, + ]) + log = [] + test.run(ExtendedTestResult(log)) + self.assertThat( + log, MatchesEvents( + ('startTest', test), + ('addError', test, { + 'traceback': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + 'traceback-1': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + }), + ('stopTest', test), + ) + ) def test_multipleErrorsCoreAndCleanupReported(self): # Errors from all failing cleanups are reported, with stopTest, # startTest inserted. - self.test = TestAddCleanup.LoggingTest('brokenTest') - self.test.addCleanup(lambda: 1/0) - self.test.addCleanup(lambda: 1/0) - self.logging_result = ExtendedTestResult() - self.test.run(self.logging_result) - self.assertEqual(['startTest', 'addError', 'stopTest'], - [event[0] for event in self.logging_result._events]) - self.assertEqual(set(['traceback', 'traceback-1', 'traceback-2']), - set(self.logging_result._events[1][2].keys())) + def broken_test(ignored): + raise RuntimeError('Deliberately broken test') + + test = make_test_case( + self.getUniqueString(), + test_body=broken_test, + cleanups=[ + lambda _: 1/0, + lambda _: 1/0, + ] + ) + log = [] + test.run(ExtendedTestResult(log)) + self.assertThat( + log, MatchesEvents( + ('startTest', test), + ('addError', test, { + 'traceback': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'RuntimeError: Deliberately broken test', + ])), + 'traceback-1': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + 'traceback-2': AsText(ContainsAll([ + 'Traceback (most recent call last):', + 'ZeroDivisionError', + ])), + }), + ('stopTest', test), + ) + ) class TestRunTestUsage(TestCase): From 77138badb2097bb0669ea4b695ba39545790bdf8 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 11:21:28 +0000 Subject: [PATCH 27/32] `throw` helper --- testtools/tests/helpers.py | 8 ++++++++ testtools/tests/test_testcase.py | 10 +++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/testtools/tests/helpers.py b/testtools/tests/helpers.py index 647e374..15dbf46 100644 --- a/testtools/tests/helpers.py +++ b/testtools/tests/helpers.py @@ -151,3 +151,11 @@ class AsText(AfterPreprocessing): def __init__(self, matcher, annotate=True): super(AsText, self).__init__( lambda log: log.as_text(), matcher, annotate=annotate) + + +def throw(function, *args, **kwargs): + """Call ``function`` with arguments and raise the result. + + Use this when you want to raise within an expression. + """ + raise function(*args, **kwargs) diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 73ba715..93c46fc 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -53,6 +53,7 @@ from testtools.tests.helpers import ( FullStackRunTest, LoggingResult, MatchesEvents, + throw, ) from testtools.tests.samplecases import ( deterministic_sample_cases_scenarios, @@ -863,10 +864,8 @@ class TestAddCleanup(TestCase): def test_keyboard_interrupt_not_caught(self): # If a cleanup raises KeyboardInterrupt, it gets reraised. - def raise_keyboard_interrupt(ignored): - raise KeyboardInterrupt() test = make_test_case( - self.getUniqueString(), cleanups=[raise_keyboard_interrupt]) + self.getUniqueString(), cleanups=[lambda _: throw(KeyboardInterrupt)]) self.assertThat(test.run, Raises(MatchesException(KeyboardInterrupt))) def test_all_errors_from_MultipleExceptions_reported(self): @@ -931,12 +930,9 @@ class TestAddCleanup(TestCase): def test_multipleErrorsCoreAndCleanupReported(self): # Errors from all failing cleanups are reported, with stopTest, # startTest inserted. - def broken_test(ignored): - raise RuntimeError('Deliberately broken test') - test = make_test_case( self.getUniqueString(), - test_body=broken_test, + test_body=lambda _: throw(RuntimeError, 'Deliberately broken test'), cleanups=[ lambda _: 1/0, lambda _: 1/0, From c38e6f2d05982f8ebb32e4f24d66e949f36d5499 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 11:33:25 +0000 Subject: [PATCH 28/32] Rewrite patch tests using make_test_case --- testtools/tests/test_testcase.py | 69 +++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index 93c46fc..5dfac81 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -1583,59 +1583,82 @@ class TestOnException(TestCase): class TestPatchSupport(TestCase): - run_test_with = FullStackRunTest + run_tests_with = FullStackRunTest class Case(TestCase): def test(self): pass + def run_test(self, test_body): + """Run a test with ``test_body`` as the body. + + :return: Whatever ``test_body`` returns. + """ + log = [] + def wrapper(case): + log.append(test_body(case)) + case = make_test_case(self.getUniqueString(), test_body=wrapper) + case.run() + return log[0] + def test_patch(self): # TestCase.patch masks obj.attribute with the new value. self.foo = 'original' - test = self.Case('test') - test.patch(self, 'foo', 'patched') - self.assertEqual('patched', self.foo) + def test_body(case): + case.patch(self, 'foo', 'patched') + return self.foo + + result = self.run_test(test_body) + self.assertThat(result, Equals('patched')) def test_patch_restored_after_run(self): # TestCase.patch masks obj.attribute with the new value, but restores # the original value after the test is finished. self.foo = 'original' - test = self.Case('test') - test.patch(self, 'foo', 'patched') - test.run() - self.assertEqual('original', self.foo) + self.run_test(lambda case: case.patch(self, 'foo', 'patched')) + self.assertThat(self.foo, Equals('original')) def test_successive_patches_apply(self): # TestCase.patch can be called multiple times per test. Each time you # call it, it overrides the original value. self.foo = 'original' - test = self.Case('test') - test.patch(self, 'foo', 'patched') - test.patch(self, 'foo', 'second') - self.assertEqual('second', self.foo) + def test_body(case): + case.patch(self, 'foo', 'patched') + case.patch(self, 'foo', 'second') + return self.foo + + result = self.run_test(test_body) + self.assertThat(result, Equals('second')) def test_successive_patches_restored_after_run(self): # TestCase.patch restores the original value, no matter how many times # it was called. self.foo = 'original' - test = self.Case('test') - test.patch(self, 'foo', 'patched') - test.patch(self, 'foo', 'second') - test.run() - self.assertEqual('original', self.foo) + def test_body(case): + case.patch(self, 'foo', 'patched') + case.patch(self, 'foo', 'second') + return self.foo + + self.run_test(test_body) + self.assertThat(self.foo, Equals('original')) def test_patch_nonexistent_attribute(self): # TestCase.patch can be used to patch a non-existent attribute. - test = self.Case('test') - test.patch(self, 'doesntexist', 'patched') - self.assertEqual('patched', self.doesntexist) + def test_body(case): + case.patch(self, 'doesntexist', 'patched') + return self.doesntexist + + result = self.run_test(test_body) + self.assertThat(result, Equals('patched')) def test_restore_nonexistent_attribute(self): # TestCase.patch can be used to patch a non-existent attribute, after # the test run, the attribute is then removed from the object. - test = self.Case('test') - test.patch(self, 'doesntexist', 'patched') - test.run() + def test_body(case): + case.patch(self, 'doesntexist', 'patched') + return self.doesntexist + + self.run_test(test_body) marker = object() value = getattr(self, 'doesntexist', marker) self.assertIs(marker, value) From 04567af7e7ed5060597d317349443725ed0c013f Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 11:33:38 +0000 Subject: [PATCH 29/32] Move _cleanups reset to _reset --- testtools/testcase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testtools/testcase.py b/testtools/testcase.py index 278239b..4238897 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -201,7 +201,6 @@ class TestCase(unittest.TestCase): """ runTest = kwargs.pop('runTest', None) super(TestCase, self).__init__(*args, **kwargs) - self._cleanups = [] self._reset() test_method = self._get_test_method() if runTest is None: @@ -224,6 +223,7 @@ class TestCase(unittest.TestCase): def _reset(self): """Reset the test case as if it had never been run.""" + self._cleanups = [] self._unique_id_gen = itertools.count(1) # Generators to ensure unique traceback ids. Maps traceback label to # iterators. From d5cc4b6a0c19219f0253691df720927ef4a1c588 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 11:40:37 +0000 Subject: [PATCH 30/32] Clarify some things --- testtools/tests/samplecases.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/testtools/tests/samplecases.py b/testtools/tests/samplecases.py index b6646cb..5d04f7d 100644 --- a/testtools/tests/samplecases.py +++ b/testtools/tests/samplecases.py @@ -47,10 +47,14 @@ def make_test_case(test_method_name, set_up=None, test_body=None, class _ConstructedTest(TestCase): - """A test case where all of the stages.""" + """A test case defined by arguments, rather than overrides.""" def __init__(self, test_method_name, set_up, test_body, tear_down, cleanups, pre_set_up, post_tear_down): + """Construct a test case. + + See ``make_test_case`` for full documentation. + """ setattr(self, test_method_name, self.test_case) super(_ConstructedTest, self).__init__(test_method_name) self._set_up = set_up @@ -80,8 +84,7 @@ def _do_nothing(case): pass -def _success(case): - pass +_success = _do_nothing def _error(case): @@ -101,7 +104,7 @@ def _expected_failure(case): def _unexpected_success(case): - case.expectFailure('arbitrary unexpected success', _success) + case.expectFailure('arbitrary unexpected success', _success, case) behaviors = [ From 15114b569e6e9e383deffe08efbe3b3d8602b338 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Mon, 7 Dec 2015 12:08:00 +0000 Subject: [PATCH 31/32] NEWS update --- NEWS | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 30f1528..5d4b263 100644 --- a/NEWS +++ b/NEWS @@ -10,7 +10,13 @@ Changes ------- * Add a new test dependency of testscenarios. (Robert Collins) - + +* ``addCleanup`` can now only be called during a test run. + (Jonathan Lange) + +* ``TestCase`` objects can now be run twice. All internal state is reset + between runs. (Jonathan Lange) + 1.8.1 ~~~~~ From 2c5fc7bc0b910a44f76567a022af8379a5739453 Mon Sep 17 00:00:00 2001 From: Jonathan Lange Date: Sun, 10 Jan 2016 09:42:48 +0000 Subject: [PATCH 32/32] Review tweaks * NEWS: 'during' -> 'within' * `throw` -> `raise_` Additionally, make `raise_` take a constructed exception, rather than constructing it ourselves. --- NEWS | 2 +- testtools/tests/helpers.py | 16 +++++++++++----- testtools/tests/test_testcase.py | 8 +++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/NEWS b/NEWS index 8735056..d0f9bb0 100644 --- a/NEWS +++ b/NEWS @@ -29,7 +29,7 @@ Changes * Add a new test dependency of testscenarios. (Robert Collins) -* ``addCleanup`` can now only be called during a test run. +* ``addCleanup`` can now only be called within a test run. (Jonathan Lange) * ``TestCase`` objects can now be run twice. All internal state is reset diff --git a/testtools/tests/helpers.py b/testtools/tests/helpers.py index 15dbf46..37b9523 100644 --- a/testtools/tests/helpers.py +++ b/testtools/tests/helpers.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2012 testtools developers. See LICENSE for details. +# Copyright (c) 2008-2016 testtools developers. See LICENSE for details. """Helpers for tests.""" @@ -30,6 +30,7 @@ try: except Exception: an_exc_info = sys.exc_info() + # Deprecated: This classes attributes are somewhat non deterministic which # leads to hard to predict tests (because Python upstream are changing things. class LoggingResult(TestResult): @@ -153,9 +154,14 @@ class AsText(AfterPreprocessing): lambda log: log.as_text(), matcher, annotate=annotate) -def throw(function, *args, **kwargs): - """Call ``function`` with arguments and raise the result. +def raise_(exception): + """Raise ``exception``. + + Useful for raising exceptions when it is inconvenient to use a statement + (e.g. in a lambda). + + :param Exception exception: An exception to raise. + :raises: Whatever exception is - Use this when you want to raise within an expression. """ - raise function(*args, **kwargs) + raise exception diff --git a/testtools/tests/test_testcase.py b/testtools/tests/test_testcase.py index b0e8440..599ac4b 100644 --- a/testtools/tests/test_testcase.py +++ b/testtools/tests/test_testcase.py @@ -53,7 +53,7 @@ from testtools.tests.helpers import ( FullStackRunTest, LoggingResult, MatchesEvents, - throw, + raise_, ) from testtools.tests.samplecases import ( deterministic_sample_cases_scenarios, @@ -885,7 +885,8 @@ class TestAddCleanup(TestCase): def test_keyboard_interrupt_not_caught(self): # If a cleanup raises KeyboardInterrupt, it gets reraised. test = make_test_case( - self.getUniqueString(), cleanups=[lambda _: throw(KeyboardInterrupt)]) + self.getUniqueString(), cleanups=[ + lambda _: raise_(KeyboardInterrupt())]) self.assertThat(test.run, Raises(MatchesException(KeyboardInterrupt))) def test_all_errors_from_MultipleExceptions_reported(self): @@ -952,7 +953,8 @@ class TestAddCleanup(TestCase): # startTest inserted. test = make_test_case( self.getUniqueString(), - test_body=lambda _: throw(RuntimeError, 'Deliberately broken test'), + test_body=lambda _: raise_( + RuntimeError('Deliberately broken test')), cleanups=[ lambda _: 1/0, lambda _: 1/0,