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
=== modified file 'NEWS'
--- NEWS 2012-05-02 03:41:51 +0000
+++ NEWS 2012-05-02 10:33:21 +0000
@@ -81,6 +81,13 @@
81 ``MatchesAll`` with keyword arguments, then this change might affect your81 ``MatchesAll`` with keyword arguments, then this change might affect your
82 test results. (Jonathan Lange)82 test results. (Jonathan Lange)
8383
84* ``ObjectFactory`` extracts out the ``getUniqueInteger`` and
85 ``getUniqueString`` behaviour from ``TestCase``. (Jonathan Lange)
86
87* ``TestCase.getUniqueInteger`` and ``TestCase.getUniqueString`` are now
88 deprecated. Use ``ObjectFactory.getUniqueInteger`` and
89 ``ObjectFactory.getUniqueString`` instead. (Jonathan Lange)
90
84Improvements91Improvements
85------------92------------
8693
8794
=== modified file 'doc/for-test-authors.rst'
--- doc/for-test-authors.rst 2012-01-29 14:08:39 +0000
+++ doc/for-test-authors.rst 2012-05-02 10:33:21 +0000
@@ -1240,17 +1240,19 @@
1240fine. However, sometimes it's useful to be able to create arbitrary objects1240fine. However, sometimes it's useful to be able to create arbitrary objects
1241at will, without having to make up silly sample data.1241at will, without having to make up silly sample data.
12421242
1243To help with this, ``testtools.TestCase`` implements creation methods called1243To help with this, ``testtools.TestCase`` comes with an ``ObjectFactory`` that
1244``getUniqueString`` and ``getUniqueInteger``. They return strings and1244you can use within your test case. ``ObjectFactory`` implements creation
1245integers that are unique within the context of the test that can be used to1245methods called ``getUniqueString`` and ``getUniqueInteger``. They return
1246assemble more complex objects. Here's a basic example where1246strings and integers that are unique within the context of the test that can
1247be used to assemble more complex objects. Here's a basic example where
1247``getUniqueString`` is used instead of saying "foo" or "bar" or whatever::1248``getUniqueString`` is used instead of saying "foo" or "bar" or whatever::
12481249
1249 class SomeTest(TestCase):1250 class SomeTest(TestCase):
12501251
1251 def test_full_name(self):1252 def test_full_name(self):
1252 first_name = self.getUniqueString()1253 factory = ObjectFactory()
1253 last_name = self.getUniqueString()1254 first_name = factory.getUniqueString()
1255 last_name = factory.getUniqueString()
1254 p = Person(first_name, last_name)1256 p = Person(first_name, last_name)
1255 self.assertEqual(p.full_name, "%s %s" % (first_name, last_name))1257 self.assertEqual(p.full_name, "%s %s" % (first_name, last_name))
12561258
@@ -1259,14 +1261,20 @@
12591261
1260 class TestCoupleLogic(TestCase):1262 class TestCoupleLogic(TestCase):
12611263
1264 def setUp(self):
1265 super(TestCoupleLogic, self).setUp()
1266 self.factory = ObjectFactory()
1267
1262 def make_arbitrary_person(self):1268 def make_arbitrary_person(self):
1263 return Person(self.getUniqueString(), self.getUniqueString())1269 return Person(
1270 self.factory.getUniqueString(),
1271 self.factory.getUniqueString())
12641272
1265 def test_get_invitation(self):1273 def test_get_invitation(self):
1266 a = self.make_arbitrary_person()1274 a = self.make_arbitrary_person()
1267 b = self.make_arbitrary_person()1275 b = self.make_arbitrary_person()
1268 couple = Couple(a, b)1276 couple = Couple(a, b)
1269 event_name = self.getUniqueString()1277 event_name = self.factory.getUniqueString()
1270 invitation = couple.get_invitation(event_name)1278 invitation = couple.get_invitation(event_name)
1271 self.assertEqual(1279 self.assertEqual(
1272 invitation,1280 invitation,
12731281
=== modified file 'testtools/__init__.py'
--- testtools/__init__.py 2012-04-13 12:33:46 +0000
+++ testtools/__init__.py 2012-05-02 10:33:21 +0000
@@ -12,6 +12,7 @@
12 'iterate_tests',12 'iterate_tests',
13 'MultipleExceptions',13 'MultipleExceptions',
14 'MultiTestResult',14 'MultiTestResult',
15 'ObjectFactory',
15 'PlaceHolder',16 'PlaceHolder',
16 'run_test_with',17 'run_test_with',
17 'Tagger',18 'Tagger',
@@ -30,6 +31,7 @@
30 'try_imports',31 'try_imports',
31 ]32 ]
3233
34from testtools.factory import ObjectFactory
33from testtools.helpers import (35from testtools.helpers import (
34 try_import,36 try_import,
35 try_imports,37 try_imports,
3638
=== added file 'testtools/factory.py'
--- testtools/factory.py 1970-01-01 00:00:00 +0000
+++ testtools/factory.py 2012-05-02 10:33:21 +0000
@@ -0,0 +1,48 @@
1# Copyright (c) 2008-2011 testtools developers. See LICENSE for details.
2
3__all__ = [
4 'ObjectFactory',
5 ]
6
7import itertools
8
9from testtools.compat import advance_iterator
10
11# XXX: Are we happy with the name ObjectFactory?
12
13# XXX: Is this a good opportunity to change the getUniqueInteger and
14# getUniqueString methods here to be get_unique_integer and get_unique_string?
15
16class ObjectFactory(object):
17
18 DEFAULT_PREFIX = 'unique'
19
20 def __init__(self, prefix=None):
21 if prefix is None:
22 prefix = self.DEFAULT_PREFIX
23 self._prefix = prefix
24 self._unique_id_gen = itertools.count(1)
25
26 def getUniqueInteger(self):
27 """Get an integer unique to this test.
28
29 Returns an integer that is guaranteed to be unique to this instance.
30 Use this when you need an arbitrary integer in your test, or as a
31 helper for custom anonymous factory methods.
32 """
33 return advance_iterator(self._unique_id_gen)
34
35 def getUniqueString(self, prefix=None):
36 """Get a string unique to this test.
37
38 Returns a string that is guaranteed to be unique to this instance. Use
39 this when you need an arbitrary string in your test, or as a helper
40 for custom anonymous factory methods.
41
42 :param prefix: The prefix of the string. If not provided, defaults
43 to the id of the tests.
44 :return: A bytestring of '<prefix>-<unique_int>'.
45 """
46 if prefix is None:
47 prefix = self._prefix
48 return '%s-%d' % (prefix, self.getUniqueInteger())
049
=== modified file 'testtools/testcase.py'
--- testtools/testcase.py 2012-04-03 07:39:43 +0000
+++ testtools/testcase.py 2012-05-02 10:33:21 +0000
@@ -28,6 +28,7 @@
28 advance_iterator,28 advance_iterator,
29 reraise,29 reraise,
30 )30 )
31from testtools.factory import ObjectFactory
31from testtools.matchers import (32from testtools.matchers import (
32 Annotate,33 Annotate,
33 Contains,34 Contains,
@@ -171,7 +172,6 @@
171 runTest = kwargs.pop('runTest', None)172 runTest = kwargs.pop('runTest', None)
172 super(TestCase, self).__init__(*args, **kwargs)173 super(TestCase, self).__init__(*args, **kwargs)
173 self._cleanups = []174 self._cleanups = []
174 self._unique_id_gen = itertools.count(1)
175 # Generators to ensure unique traceback ids. Maps traceback label to175 # Generators to ensure unique traceback ids. Maps traceback label to
176 # iterators.176 # iterators.
177 self._traceback_id_gens = {}177 self._traceback_id_gens = {}
@@ -196,6 +196,7 @@
196 if sys.version_info < (2, 6):196 if sys.version_info < (2, 6):
197 # Catch old-style string exceptions with None as the instance197 # Catch old-style string exceptions with None as the instance
198 self.exception_handlers.append((type(None), self._report_error))198 self.exception_handlers.append((type(None), self._report_error))
199 self.__factory = ObjectFactory(self.id())
199200
200 def __eq__(self, other):201 def __eq__(self, other):
201 eq = getattr(unittest.TestCase, '__eq__', None)202 eq = getattr(unittest.TestCase, '__eq__', None)
@@ -447,16 +448,20 @@
447 raise _UnexpectedSuccess(reason)448 raise _UnexpectedSuccess(reason)
448449
449 def getUniqueInteger(self):450 def getUniqueInteger(self):
450 """Get an integer unique to this test.451 """DEPRECATED - Get an integer unique to this test.
452
453 Use ``ObjectFactory.getUniqueInteger()`` instead.
451454
452 Returns an integer that is guaranteed to be unique to this instance.455 Returns an integer that is guaranteed to be unique to this instance.
453 Use this when you need an arbitrary integer in your test, or as a456 Use this when you need an arbitrary integer in your test, or as a
454 helper for custom anonymous factory methods.457 helper for custom anonymous factory methods.
455 """458 """
456 return advance_iterator(self._unique_id_gen)459 return self.factory.getUniqueInteger()
457460
458 def getUniqueString(self, prefix=None):461 def getUniqueString(self, prefix=None):
459 """Get a string unique to this test.462 """DEPRECATED - Get a string unique to this test.
463
464 Use ``ObjectFactory.getUniqueString()`` instead.
460465
461 Returns a string that is guaranteed to be unique to this instance. Use466 Returns a string that is guaranteed to be unique to this instance. Use
462 this when you need an arbitrary string in your test, or as a helper467 this when you need an arbitrary string in your test, or as a helper
@@ -466,9 +471,7 @@
466 to the id of the tests.471 to the id of the tests.
467 :return: A bytestring of '<prefix>-<unique_int>'.472 :return: A bytestring of '<prefix>-<unique_int>'.
468 """473 """
469 if prefix is None:474 return self.factory.getUniqueString(prefix=prefix)
470 prefix = self.id()
471 return '%s-%d' % (prefix, self.getUniqueInteger())
472475
473 def onException(self, exc_info, tb_label='traceback'):476 def onException(self, exc_info, tb_label='traceback'):
474 """Called when an exception propogates from test code.477 """Called when an exception propogates from test code.
475478
=== modified file 'testtools/tests/__init__.py'
--- testtools/tests/__init__.py 2012-04-11 11:00:52 +0000
+++ testtools/tests/__init__.py 2012-05-02 10:33:21 +0000
@@ -12,6 +12,7 @@
12 test_content_type,12 test_content_type,
13 test_deferredruntest,13 test_deferredruntest,
14 test_distutilscmd,14 test_distutilscmd,
15 test_factory,
15 test_fixturesupport,16 test_fixturesupport,
16 test_helpers,17 test_helpers,
17 test_matchers,18 test_matchers,
@@ -30,6 +31,7 @@
30 test_content_type,31 test_content_type,
31 test_deferredruntest,32 test_deferredruntest,
32 test_distutilscmd,33 test_distutilscmd,
34 test_factory,
33 test_fixturesupport,35 test_fixturesupport,
34 test_helpers,36 test_helpers,
35 test_matchers,37 test_matchers,
3638
=== added file 'testtools/tests/test_factory.py'
--- testtools/tests/test_factory.py 1970-01-01 00:00:00 +0000
+++ testtools/tests/test_factory.py 2012-05-02 10:33:21 +0000
@@ -0,0 +1,47 @@
1from testtools import TestCase
2from testtools.factory import ObjectFactory
3from testtools.matchers import Equals
4
5
6class TestFactory(TestCase):
7
8 def test_getUniqueInteger(self):
9 # getUniqueInteger returns an integer that increments each time you
10 # call it.
11 factory = ObjectFactory()
12 one = factory.getUniqueInteger()
13 self.assertEqual(1, one)
14 two = factory.getUniqueInteger()
15 self.assertEqual(2, two)
16
17 def test_getUniqueString_default_prefix(self):
18 # If no other parameters are given, getUniqueString returns a string
19 # starting with the default prefix followed by a unique integer.
20 factory = ObjectFactory()
21 name_one = factory.getUniqueString()
22 self.assertEqual('%s-%d' % (factory.DEFAULT_PREFIX, 1), name_one)
23 name_two = factory.getUniqueString()
24 self.assertEqual('%s-%d' % (factory.DEFAULT_PREFIX, 2), name_two)
25
26 def test_getUniqueString_early_prefix(self):
27 # An optional prefix can be given to the factory. If so, then all
28 # getUniqueString calls use that as a prefix by default.
29 factory = ObjectFactory(prefix=self.id())
30 name_one = factory.getUniqueString()
31 self.assertEqual('%s-%d' % (self.id(), 1), name_one)
32 name_two = factory.getUniqueString()
33 self.assertEqual('%s-%d' % (self.id(), 2), name_two)
34
35 def test_getUniqueString_late_prefix(self):
36 # If getUniqueString is given an argument, it uses that argument as
37 # the prefix of the unique string, rather than the test id.
38 factory = ObjectFactory()
39 name_one = factory.getUniqueString('foo')
40 self.assertThat(name_one, Equals('foo-1'))
41 name_two = factory.getUniqueString('bar')
42 self.assertThat(name_two, Equals('bar-2'))
43
44
45def test_suite():
46 from unittest import TestLoader
47 return TestLoader().loadTestsFromName(__name__)

Subscribers

People subscribed via source and target branches