Merge lp:~jml/testtools/extract-factory into lp:~testtools-committers/testtools/trunk

Proposed by Jonathan Lange
Status: Needs review
Proposed branch: lp:~jml/testtools/extract-factory
Merge into: lp:~testtools-committers/testtools/trunk
Diff against target: 278 lines (+132/-15)
7 files modified
NEWS (+7/-0)
doc/for-test-authors.rst (+16/-8)
testtools/__init__.py (+2/-0)
testtools/factory.py (+48/-0)
testtools/testcase.py (+10/-7)
testtools/tests/__init__.py (+2/-0)
testtools/tests/test_factory.py (+47/-0)
To merge this branch: bzr merge lp:~jml/testtools/extract-factory
Reviewer Review Type Date Requested Status
Robert Collins Approve
Review via email: mp+85833@code.launchpad.net

Description of the change

This branch follows the advice that I recall receiving from Rob ages ago: it extracts a separate factory class out of TestCase.

It's fairly straightforward, but there are a few questions that came to mind while I was doing the work:

testtools/factory.py:11:
  XXX: Are we happy with the name ObjectFactory?

testtools/factory.py:13:
  XXX: Is this a good opportunity to change the getUniqueInteger and
  getUniqueString methods here to be get_unique_integer and get_unique_string?

testtools/testcase.py:196:
  XXX: We could have a class variable factory_factory (except with a
  better name) and then instead write:
      self.factory = self.factory_factory(self)
  Which would allow others to plugin in their factories more easily,
  rather than using the TestCaseWithFactory mixin pattern. Just a
  thought.

Would be interested in opinions.

jml

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :
Download full text (10.9 KiB)

Does the factory need to be exposed at all?

On 15/12/2011 10:19 PM, "Jonathan Lange" <email address hidden> wrote:

Jonathan Lange has proposed merging lp:~jml/testtools/extract-factory into
lp:testtools.

Requested ...
Your team testtools developers is subscribed to branch lp:testtools.

=== modified file 'NEWS'
--- NEWS 2011-12-05 15:33:37 +0000
+++ NEWS 2011-12-15 11:18:22 +0000
@@ -14,6 +14,14 @@
  ``MatchesAll`` with keyword arguments, then this change might affect your
  test results. (Jonathan Lange)

+* ``TestCase`` now has a ``factory`` attribute, set to an instance of
+ ``ObjectFactory``. It now uses this instance for generating unique
strings
+ and integers. (Jonathan Lange)
+
+* ``TestCase.getUniqueInteger`` and ``TestCase.getUniqueString`` are now
+ deprecated. Use ``TestCase.factory.getUniqueInteger`` and
+ ``TestCase.factory.getUniqueString`` instead. (Jonathan Lange)
+
 Improvements
 ------------

=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst 2011-12-07 11:32:45 +0000
+++ doc/for-test-authors.rst 2011-12-15 11:18:22 +0000
@@ -1240,17 +1240,19 @@
 fine. However, sometimes it's useful to be able to create arbitrary
objects
 at will, without having to make up silly sample data.

-To help with this, ``testtools.TestCase`` implements creation methods
called
-``getUniqueString`` and ``getUniqueInteger``. They return strings and
-integers that are unique within the context of the test that can be used to
-assemble more complex objects. Here's a basic example where
-``getUniqueString`` is used instead of saying "foo" or "bar" or whatever::
+To help with this, ``testtools.TestCase`` comes with an ``ObjectFactory``
that
+you can access as the ``factory`` attribute within your test
+case. ``ObjectFactory`` implements creation methods called
``getUniqueString``
+and ``getUniqueInteger``. They return strings and integers that are unique
+within the context of the test that can be used to assemble more complex
+objects. Here's a basic example where ``getUniqueString`` is used instead
of
+saying "foo" or "bar" or whatever::

  class SomeTest(TestCase):

      def test_full_name(self):
- first_name = self.getUniqueString()
- last_name = self.getUniqueString()
+ first_name = self.factory.getUniqueString()
+ last_name = self.factory.getUniqueString()
          p = Person(first_name, last_name)
          self.assertEqual(p.full_name, "%s %s" % (first_name, last_name))

@@ -1260,13 +1262,15 @@
  class TestCoupleLogic(TestCase):

      def make_arbitrary_person(self):
- return Person(self.getUniqueString(), self.getUniqueString())
+ return Person(
+ self.factory.getUniqueString(),
+ self.factory.getUniqueString())

      def test_get_invitation(self):
          a = self.make_arbitrary_person()
          b = self.make_arbitrary_person()
          couple = Couple(a, b)
- event_name = self.getUniqueString()
+ event_name = self.factory.getUniqueString()
          invitation = couple.get_invitation(event_name)
          self.assertEqual(
              invitation,

=== modified file 'testtools/__init_...

Revision history for this message
Jonathan Lange (jml) wrote :

On Thu, Dec 15, 2011 at 1:13 PM, Robert Collins
<email address hidden> wrote:
> Does the factory need to be exposed at all?

As in, does it need to be in the public API?

Not really, but it would allow Launchpad to delete a little bit of
code from its ObjectFactory, and make it easier for other projects to
build their own domain-specific factories.

jml

Revision history for this message
Robert Collins (lifeless) wrote :

I meant 'does the factory need to be a test case attribute'. To me, the factory is a multipurpose fixture, and fixtures are very usable (e.g. via scenarios) without being exposed on the testcase by default.

From the point of view of compat with prior testtools releases we can't drop our additional methods, but we can avoid adding an overly specific factory instance on every TestCase object. So putting it on e.g. __factory. That would let the new factory be used by other testing styles (e.g. nose), or evolved separately to TestCase.

Beyond that there is an interesting discussion about whether global factories are a good idea, or whether per test state is a better idea, and a little orthogonally, whether the factory should be a Fixture (which can then provide details etc) or an adhoc API.

So the .factory is the only thing that really needs fixing, I'll mark this approved if you change factory to __factory.

review: Approve
lp:~jml/testtools/extract-factory updated
242. By Jonathan Lange

Unexpose TestCase.factory, and recommend that people use ObjectFactory for themselves.

Revision history for this message
Jonathan Lange (jml) wrote :

On Wed, May 2, 2012 at 4:56 AM, Robert Collins
<email address hidden> wrote:
> Review: Approve
>
> I meant 'does the factory need to be a test case attribute'. To me, the factory is a multipurpose fixture, and fixtures are very usable (e.g. via scenarios) without being exposed on the testcase by default.
>
> >From the point of view of compat with prior testtools releases we can't drop our additional methods, but we can avoid adding an overly specific factory instance on every TestCase object. So putting it on e.g. __factory. That would let the new factory be used by other testing styles (e.g. nose), or evolved separately to TestCase.
>

OK. Thanks for clarifying.

FWIW, the price of this is making the factory slightly less convenient
to start using.

e.g. from

  x = self.factory.getUniqueString()

To

  factory = ObjectFactory()
  x = factory.getUniqueString()

To perhaps even

  def setUp(self):
    super(MyTestCase, self).setUp()
    self.factory = factory

  def test_something(self):
    x = self.factory.getUniqueString()

The structural correctness is probably worth this cost for now.

> Beyond that there is an interesting discussion about whether global factories are a good idea, or whether per test state is a better idea, and a little orthogonally, whether the factory should be a Fixture (which can then provide details etc) or an adhoc API.

I'd be interested in having that discussion ... later :)

>
> So the .factory is the only thing that really needs fixing, I'll mark this approved if you change factory to __factory.

I really don't like double underscores. But sure, let's get this
landed. Changing to drop one is easy and very low risk.

jml

Revision history for this message
Robert Collins (lifeless) wrote :

So I think this became good to merge but we need to port it to git.

Unmerged revisions

242. By Jonathan Lange

Unexpose TestCase.factory, and recommend that people use ObjectFactory for themselves.

241. By Jonathan Lange

Documentation update.

240. By Jonathan Lange

News.

239. By Jonathan Lange

Some thoughts on options
Deprecation
Expose ObjectFactory at the top-level.

238. By Jonathan Lange

Use ObjectFactory rather than implementing it ourselves.

237. By Jonathan Lange

Extract out a factory.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2012-05-02 03:41:51 +0000
3+++ NEWS 2012-05-02 10:33:21 +0000
4@@ -81,6 +81,13 @@
5 ``MatchesAll`` with keyword arguments, then this change might affect your
6 test results. (Jonathan Lange)
7
8+* ``ObjectFactory`` extracts out the ``getUniqueInteger`` and
9+ ``getUniqueString`` behaviour from ``TestCase``. (Jonathan Lange)
10+
11+* ``TestCase.getUniqueInteger`` and ``TestCase.getUniqueString`` are now
12+ deprecated. Use ``ObjectFactory.getUniqueInteger`` and
13+ ``ObjectFactory.getUniqueString`` instead. (Jonathan Lange)
14+
15 Improvements
16 ------------
17
18
19=== modified file 'doc/for-test-authors.rst'
20--- doc/for-test-authors.rst 2012-01-29 14:08:39 +0000
21+++ doc/for-test-authors.rst 2012-05-02 10:33:21 +0000
22@@ -1240,17 +1240,19 @@
23 fine. However, sometimes it's useful to be able to create arbitrary objects
24 at will, without having to make up silly sample data.
25
26-To help with this, ``testtools.TestCase`` implements creation methods called
27-``getUniqueString`` and ``getUniqueInteger``. They return strings and
28-integers that are unique within the context of the test that can be used to
29-assemble more complex objects. Here's a basic example where
30+To help with this, ``testtools.TestCase`` comes with an ``ObjectFactory`` that
31+you can use within your test case. ``ObjectFactory`` implements creation
32+methods called ``getUniqueString`` and ``getUniqueInteger``. They return
33+strings and integers that are unique within the context of the test that can
34+be used to assemble more complex objects. Here's a basic example where
35 ``getUniqueString`` is used instead of saying "foo" or "bar" or whatever::
36
37 class SomeTest(TestCase):
38
39 def test_full_name(self):
40- first_name = self.getUniqueString()
41- last_name = self.getUniqueString()
42+ factory = ObjectFactory()
43+ first_name = factory.getUniqueString()
44+ last_name = factory.getUniqueString()
45 p = Person(first_name, last_name)
46 self.assertEqual(p.full_name, "%s %s" % (first_name, last_name))
47
48@@ -1259,14 +1261,20 @@
49
50 class TestCoupleLogic(TestCase):
51
52+ def setUp(self):
53+ super(TestCoupleLogic, self).setUp()
54+ self.factory = ObjectFactory()
55+
56 def make_arbitrary_person(self):
57- return Person(self.getUniqueString(), self.getUniqueString())
58+ return Person(
59+ self.factory.getUniqueString(),
60+ self.factory.getUniqueString())
61
62 def test_get_invitation(self):
63 a = self.make_arbitrary_person()
64 b = self.make_arbitrary_person()
65 couple = Couple(a, b)
66- event_name = self.getUniqueString()
67+ event_name = self.factory.getUniqueString()
68 invitation = couple.get_invitation(event_name)
69 self.assertEqual(
70 invitation,
71
72=== modified file 'testtools/__init__.py'
73--- testtools/__init__.py 2012-04-13 12:33:46 +0000
74+++ testtools/__init__.py 2012-05-02 10:33:21 +0000
75@@ -12,6 +12,7 @@
76 'iterate_tests',
77 'MultipleExceptions',
78 'MultiTestResult',
79+ 'ObjectFactory',
80 'PlaceHolder',
81 'run_test_with',
82 'Tagger',
83@@ -30,6 +31,7 @@
84 'try_imports',
85 ]
86
87+from testtools.factory import ObjectFactory
88 from testtools.helpers import (
89 try_import,
90 try_imports,
91
92=== added file 'testtools/factory.py'
93--- testtools/factory.py 1970-01-01 00:00:00 +0000
94+++ testtools/factory.py 2012-05-02 10:33:21 +0000
95@@ -0,0 +1,48 @@
96+# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
97+
98+__all__ = [
99+ 'ObjectFactory',
100+ ]
101+
102+import itertools
103+
104+from testtools.compat import advance_iterator
105+
106+# XXX: Are we happy with the name ObjectFactory?
107+
108+# XXX: Is this a good opportunity to change the getUniqueInteger and
109+# getUniqueString methods here to be get_unique_integer and get_unique_string?
110+
111+class ObjectFactory(object):
112+
113+ DEFAULT_PREFIX = 'unique'
114+
115+ def __init__(self, prefix=None):
116+ if prefix is None:
117+ prefix = self.DEFAULT_PREFIX
118+ self._prefix = prefix
119+ self._unique_id_gen = itertools.count(1)
120+
121+ def getUniqueInteger(self):
122+ """Get an integer unique to this test.
123+
124+ Returns an integer that is guaranteed to be unique to this instance.
125+ Use this when you need an arbitrary integer in your test, or as a
126+ helper for custom anonymous factory methods.
127+ """
128+ return advance_iterator(self._unique_id_gen)
129+
130+ def getUniqueString(self, prefix=None):
131+ """Get a string unique to this test.
132+
133+ Returns a string that is guaranteed to be unique to this instance. Use
134+ this when you need an arbitrary string in your test, or as a helper
135+ for custom anonymous factory methods.
136+
137+ :param prefix: The prefix of the string. If not provided, defaults
138+ to the id of the tests.
139+ :return: A bytestring of '<prefix>-<unique_int>'.
140+ """
141+ if prefix is None:
142+ prefix = self._prefix
143+ return '%s-%d' % (prefix, self.getUniqueInteger())
144
145=== modified file 'testtools/testcase.py'
146--- testtools/testcase.py 2012-04-03 07:39:43 +0000
147+++ testtools/testcase.py 2012-05-02 10:33:21 +0000
148@@ -28,6 +28,7 @@
149 advance_iterator,
150 reraise,
151 )
152+from testtools.factory import ObjectFactory
153 from testtools.matchers import (
154 Annotate,
155 Contains,
156@@ -171,7 +172,6 @@
157 runTest = kwargs.pop('runTest', None)
158 super(TestCase, self).__init__(*args, **kwargs)
159 self._cleanups = []
160- self._unique_id_gen = itertools.count(1)
161 # Generators to ensure unique traceback ids. Maps traceback label to
162 # iterators.
163 self._traceback_id_gens = {}
164@@ -196,6 +196,7 @@
165 if sys.version_info < (2, 6):
166 # Catch old-style string exceptions with None as the instance
167 self.exception_handlers.append((type(None), self._report_error))
168+ self.__factory = ObjectFactory(self.id())
169
170 def __eq__(self, other):
171 eq = getattr(unittest.TestCase, '__eq__', None)
172@@ -447,16 +448,20 @@
173 raise _UnexpectedSuccess(reason)
174
175 def getUniqueInteger(self):
176- """Get an integer unique to this test.
177+ """DEPRECATED - Get an integer unique to this test.
178+
179+ Use ``ObjectFactory.getUniqueInteger()`` instead.
180
181 Returns an integer that is guaranteed to be unique to this instance.
182 Use this when you need an arbitrary integer in your test, or as a
183 helper for custom anonymous factory methods.
184 """
185- return advance_iterator(self._unique_id_gen)
186+ return self.factory.getUniqueInteger()
187
188 def getUniqueString(self, prefix=None):
189- """Get a string unique to this test.
190+ """DEPRECATED - Get a string unique to this test.
191+
192+ Use ``ObjectFactory.getUniqueString()`` instead.
193
194 Returns a string that is guaranteed to be unique to this instance. Use
195 this when you need an arbitrary string in your test, or as a helper
196@@ -466,9 +471,7 @@
197 to the id of the tests.
198 :return: A bytestring of '<prefix>-<unique_int>'.
199 """
200- if prefix is None:
201- prefix = self.id()
202- return '%s-%d' % (prefix, self.getUniqueInteger())
203+ return self.factory.getUniqueString(prefix=prefix)
204
205 def onException(self, exc_info, tb_label='traceback'):
206 """Called when an exception propogates from test code.
207
208=== modified file 'testtools/tests/__init__.py'
209--- testtools/tests/__init__.py 2012-04-11 11:00:52 +0000
210+++ testtools/tests/__init__.py 2012-05-02 10:33:21 +0000
211@@ -12,6 +12,7 @@
212 test_content_type,
213 test_deferredruntest,
214 test_distutilscmd,
215+ test_factory,
216 test_fixturesupport,
217 test_helpers,
218 test_matchers,
219@@ -30,6 +31,7 @@
220 test_content_type,
221 test_deferredruntest,
222 test_distutilscmd,
223+ test_factory,
224 test_fixturesupport,
225 test_helpers,
226 test_matchers,
227
228=== added file 'testtools/tests/test_factory.py'
229--- testtools/tests/test_factory.py 1970-01-01 00:00:00 +0000
230+++ testtools/tests/test_factory.py 2012-05-02 10:33:21 +0000
231@@ -0,0 +1,47 @@
232+from testtools import TestCase
233+from testtools.factory import ObjectFactory
234+from testtools.matchers import Equals
235+
236+
237+class TestFactory(TestCase):
238+
239+ def test_getUniqueInteger(self):
240+ # getUniqueInteger returns an integer that increments each time you
241+ # call it.
242+ factory = ObjectFactory()
243+ one = factory.getUniqueInteger()
244+ self.assertEqual(1, one)
245+ two = factory.getUniqueInteger()
246+ self.assertEqual(2, two)
247+
248+ def test_getUniqueString_default_prefix(self):
249+ # If no other parameters are given, getUniqueString returns a string
250+ # starting with the default prefix followed by a unique integer.
251+ factory = ObjectFactory()
252+ name_one = factory.getUniqueString()
253+ self.assertEqual('%s-%d' % (factory.DEFAULT_PREFIX, 1), name_one)
254+ name_two = factory.getUniqueString()
255+ self.assertEqual('%s-%d' % (factory.DEFAULT_PREFIX, 2), name_two)
256+
257+ def test_getUniqueString_early_prefix(self):
258+ # An optional prefix can be given to the factory. If so, then all
259+ # getUniqueString calls use that as a prefix by default.
260+ factory = ObjectFactory(prefix=self.id())
261+ name_one = factory.getUniqueString()
262+ self.assertEqual('%s-%d' % (self.id(), 1), name_one)
263+ name_two = factory.getUniqueString()
264+ self.assertEqual('%s-%d' % (self.id(), 2), name_two)
265+
266+ def test_getUniqueString_late_prefix(self):
267+ # If getUniqueString is given an argument, it uses that argument as
268+ # the prefix of the unique string, rather than the test id.
269+ factory = ObjectFactory()
270+ name_one = factory.getUniqueString('foo')
271+ self.assertThat(name_one, Equals('foo-1'))
272+ name_two = factory.getUniqueString('bar')
273+ self.assertThat(name_two, Equals('bar-2'))
274+
275+
276+def test_suite():
277+ from unittest import TestLoader
278+ return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches