diff --git a/apps/patchwork/tests/factory.py b/apps/patchwork/tests/factory.py
new file mode 100644
index 0000000..a750e85
--- /dev/null
+++ b/apps/patchwork/tests/factory.py
@@ -0,0 +1,111 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2011 Jeremy Kerr <jk@ozlabs.org>
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from datetime import datetime
+import email
+from itertools import count
+from random import randint
+
+from django.contrib.auth.models import User
+
+from patchwork.models import Bundle, Patch, Person, Project, State
+
+
+class ObjectFactory(object):
+    """Factory methods for creating basic Python objects."""
+
+    def __init__(self):
+        # Initialise the unique identifier.
+        self.integer = count(randint(0, 1000000))
+
+    def getUniqueEmailAddress(self):
+        return "%s@example.com" % self.getUniqueString('email')
+
+    def getUniqueString(self, prefix=None):
+        """Return a string unique to this factory instance.
+
+        :param prefix: Used as a prefix for the unique string. If unspecified,
+            defaults to 'generic-string'.
+        """
+        if prefix is None:
+            prefix = "generic-string"
+        string = "%s%s" % (prefix, self.getUniqueInteger())
+        return string.lower()
+
+    def getUniqueInteger(self):
+        """Return an integer unique to this factory instance."""
+        return self.integer.next()
+
+    def makeUser(self):
+        userid = password = self.getUniqueString()
+        user = User.objects.create_user(
+            userid, self.getUniqueEmailAddress(), password)
+        user.save()
+        return user
+
+    def makeProject(self):
+        name = self.getUniqueString()
+        project = Project(
+            linkname=name, name=name,
+            listid=self.getUniqueString(),
+            listemail=self.getUniqueEmailAddress())
+        project.save()
+        return project
+
+    def makePerson(self, is_user=True):
+        person = Person(
+            email=self.getUniqueEmailAddress(), name=self.getUniqueString())
+        person.save()
+        if is_user:
+            # Must create the user after the person is created or else
+            # create_user() will trigger the creation of a person.
+            user = User.objects.create_user(
+                person.name, person.email, password=None)
+            user.save()
+        # Re-fetch the person object so that our callsite sees the link to the
+        # newly created user.
+        person = Person.objects.get(email=person.email)
+        return person
+
+    def makeBundle(self, patches=None):
+        if patches is None:
+            patches = [self.makePatch()]
+        bundle = Bundle(
+            owner=self.makeUser(), project=self.makeProject(),
+            name=self.getUniqueString())
+        bundle.save()
+        for patch in patches:
+            bundle.append_patch(patch)
+        bundle.save()
+        return bundle
+
+    def makePatch(self, project=None, submitter=None, date=None):
+        if date is None:
+            date = datetime.now()
+        if project is None:
+            project = self.makeProject()
+        if submitter is None:
+            submitter = self.makePerson()
+        msgid = email.utils.make_msgid(idstring=self.getUniqueString())
+        patch = Patch(
+            project=project, msgid=msgid, name=self.getUniqueString(),
+            submitter=submitter, date=date,
+            state=State.objects.get(name='New'))
+        patch.save()
+        return patch
