Merge lp:~julian+/testscenarios/testscenarios into lp:~testtools-committers/testscenarios/trunk

Proposed by Julian Berman
Status: Needs review
Proposed branch: lp:~julian+/testscenarios/testscenarios
Merge into: lp:~testtools-committers/testscenarios/trunk
Diff against target: 199 lines (+118/-2)
5 files modified
lib/testscenarios/__init__.py (+2/-0)
lib/testscenarios/scenarios.py (+52/-0)
lib/testscenarios/tests/test_scenarios.py (+61/-0)
lib/testscenarios/tests/test_testcase.py (+1/-1)
setup.py (+2/-1)
To merge this branch: bzr merge lp:~julian+/testscenarios/testscenarios
Reviewer Review Type Date Requested Status
John Vandenberg (community) Needs Fixing
testtools committers Pending
Review via email: mp+182255@code.launchpad.net

Description of the change

Add with_scenarios, a (class) decorator to create individual test methods out of test scenarios.

To post a comment you must log in.
23. By Julian Berman <email address hidden>

Use setuptools and declare the testtools dependency.

24. By Julian Berman <email address hidden>

Use dir directly to avoid inspect.getmembers issues on python2.6

Revision history for this message
John Vandenberg (jayvdb) wrote :

the setup.py patch means this doesnt merge nicely on top of the latest released version 4.0.

I initially used @with_scenarios on a TestCase that subclassed TestWithScenarios, which works but causes all tests to be done many times. It would be good to cause an error when cls is a subclass of TestWithScenarios.

review: Needs Fixing
Revision history for this message
John Vandenberg (jayvdb) :
Revision history for this message
Robert Collins (lifeless) wrote :

Hi, sorry for the disruption, but I've finally moved testscenarios to github: https://github.com/testing-cabal/testscenarios/

@jayvdb if you're interested in this patch, perhaps you could turn it into a PR on github?

Unmerged revisions

24. By Julian Berman <email address hidden>

Use dir directly to avoid inspect.getmembers issues on python2.6

23. By Julian Berman <email address hidden>

Use setuptools and declare the testtools dependency.

22. By Julian Berman <email address hidden>

Add with_scenarios, generating test methods for each scenario.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/testscenarios/__init__.py'
2--- lib/testscenarios/__init__.py 2012-04-04 10:46:52 +0000
3+++ lib/testscenarios/__init__.py 2015-02-19 17:21:31 +0000
4@@ -49,6 +49,7 @@
5 'load_tests_apply_scenarios',
6 'multiply_scenarios',
7 'per_module_scenarios',
8+ 'with_scenarios',
9 ]
10
11
12@@ -60,6 +61,7 @@
13 load_tests_apply_scenarios,
14 multiply_scenarios,
15 per_module_scenarios,
16+ with_scenarios,
17 )
18 from testscenarios.testcase import TestWithScenarios, WithScenarios
19
20
21=== modified file 'lib/testscenarios/scenarios.py'
22--- lib/testscenarios/scenarios.py 2012-04-04 10:02:46 +0000
23+++ lib/testscenarios/scenarios.py 2015-02-19 17:21:31 +0000
24@@ -23,10 +23,13 @@
25 'multiply_scenarios',
26 ]
27
28+from functools import wraps
29 from itertools import (
30 chain,
31 product,
32 )
33+from operator import methodcaller
34+import inspect
35 import sys
36 import unittest
37
38@@ -164,3 +167,52 @@
39 short_name,
40 {attribute_name: mod}))
41 return scenarios
42+
43+
44+def with_scenarios(prefix="test"):
45+ """Mutate the decorated TestCase, producing methods for each scenario.
46+
47+ :param str prefix: the prefix to match for test methods
48+
49+ """
50+ def _populate_scenarios(cls):
51+ scenarios = getattr(cls, "scenarios", None)
52+ if scenarios is None:
53+ return cls
54+
55+ tests = [
56+ (name, getattr(cls, name, None))
57+ for name in dir(cls) if name.startswith(prefix)
58+ ]
59+
60+ for name, test in tests:
61+ # Sometimes dir lies, which we caught by using None above
62+ if test is None:
63+ continue
64+
65+ delattr(cls, name)
66+ for scenario_name, scenario_params in scenarios:
67+ _test = _make_test(name, test, scenario_name, scenario_params)
68+ setattr(cls, _test.__name__, _test)
69+ return cls
70+ return _populate_scenarios
71+
72+
73+def _make_test(name, test, scenario_name, scenario_params):
74+ """Create a test that wraps the given test under the given scenario.
75+
76+ :param str name: the name of the test
77+ :param callable test: the test method
78+ :param str scenario_name: the scenario
79+ :param dict scenario_params: the scenario's parameters
80+
81+ """
82+
83+ @wraps(test)
84+ def _test(self, *args, **kwargs):
85+ for k, v in scenario_params.iteritems():
86+ setattr(self, k, v)
87+ return test(self, *args, **kwargs)
88+
89+ _test.__name__ = "%s_%s" % (name, scenario_name)
90+ return _test
91
92=== modified file 'lib/testscenarios/tests/test_scenarios.py'
93--- lib/testscenarios/tests/test_scenarios.py 2012-04-04 10:02:46 +0000
94+++ lib/testscenarios/tests/test_scenarios.py 2015-02-19 17:21:31 +0000
95@@ -24,6 +24,7 @@
96 generate_scenarios,
97 load_tests_apply_scenarios,
98 multiply_scenarios,
99+ with_scenarios,
100 )
101 import testtools
102 from testtools.tests.helpers import LoggingResult
103@@ -259,3 +260,63 @@
104 ('unittest', {'the_module': unittest}),
105 ('nonexistent', {'the_module': None}),
106 ])
107+
108+
109+class TestWithScenariosDecorator(testtools.TestCase):
110+ def test_with_scenarios(self):
111+ @with_scenarios()
112+ class TestFoo(object):
113+ scenarios = [
114+ ("first", {"a" : 1}),
115+ ("second", {"a" : 2}),
116+ ]
117+
118+ def test_bar(self):
119+ if self.a == 2:
120+ raise ImportError("Yup, it's 2")
121+
122+ def test_baz(self):
123+ if self.a == 1:
124+ raise ImportError("Yup, it's 1")
125+
126+ self.assertEqual(
127+ sorted(name for name in dir(TestFoo) if name.startswith("test")),
128+ [
129+ "test_bar_first",
130+ "test_bar_second",
131+ "test_baz_first",
132+ "test_baz_second",
133+ ],
134+ )
135+
136+ TestFoo().test_bar_first()
137+ TestFoo().test_baz_second()
138+
139+ self.assertRaises(ImportError, TestFoo().test_bar_second)
140+ self.assertRaises(ImportError, TestFoo().test_baz_first)
141+
142+ def test_with_scenarios_prefix(self):
143+ @with_scenarios(prefix="test_")
144+ class TestFoo(object):
145+ scenarios = [("a", {})]
146+ def testBar(self):
147+ pass
148+
149+ def test_foo(self):
150+ pass
151+
152+ self.assertEqual(
153+ sorted(name for name in dir(TestFoo) if name.startswith("test")),
154+ ["testBar", "test_foo_a"],
155+ )
156+
157+ def test_with_scenarios_no_scenarios(self):
158+ @with_scenarios()
159+ class TestFoo(object):
160+ def test_bar(self):
161+ pass
162+
163+ self.assertEqual(
164+ sorted(name for name in dir(TestFoo) if name.startswith("test")),
165+ ["test_bar"],
166+ )
167
168=== modified file 'lib/testscenarios/tests/test_testcase.py'
169--- lib/testscenarios/tests/test_testcase.py 2012-04-04 10:45:22 +0000
170+++ lib/testscenarios/tests/test_testcase.py 2015-02-19 17:21:31 +0000
171@@ -21,7 +21,7 @@
172 from testtools.tests.helpers import LoggingResult
173
174
175-class TestTestWithScenarios(testtools.TestCase):
176+class TestTestWithScenarios(testscenarios.WithScenarios, testtools.TestCase):
177
178 scenarios = testscenarios.scenarios.per_module_scenarios(
179 'impl', (('unittest', 'unittest'), ('unittest2', 'unittest2')))
180
181=== modified file 'setup.py'
182--- setup.py 2012-04-04 10:46:52 +0000
183+++ setup.py 2015-02-19 17:21:31 +0000
184@@ -1,6 +1,6 @@
185 #!/usr/bin/env python
186
187-from distutils.core import setup
188+from setuptools import setup
189 import os.path
190
191 description = file(os.path.join(os.path.dirname(__file__), 'README'), 'rb').read()
192@@ -12,6 +12,7 @@
193 maintainer="Robert Collins",
194 maintainer_email="robertc@robertcollins.net",
195 url="https://launchpad.net/testscenarios",
196+ install_requires=["testtools"],
197 packages=['testscenarios', 'testscenarios.tests'],
198 package_dir = {'':'lib'},
199 classifiers = [

Subscribers

People subscribed via source and target branches