Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit b9f513f9327c412f148b023885895df953ce9262 0 parents
@bfirsh bfirsh authored
2  .gitignore
@@ -0,0 +1,2 @@
+*.pyc
+build/
1  AUTHORS
@@ -0,0 +1 @@
+Ben Firshman <ben@firshman.co.uk>
24 LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2009, Ben Firshman
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * The names of its contributors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
73 README.markdown
@@ -0,0 +1,73 @@
+pytest_django
+=============
+
+pytest_django is a plugin for [py.test](http://pytest.org/) that provides a set of useful tools for testing Django applications.
+
+Requires:
+
+ * Django 1.1
+ * py.test 1.0.0
+
+Installation
+------------
+
+ $ python setup.py install
+
+Then simply create a `conftest.py` file in the root of your Django project
+containing:
+
+ pytest_plugins = ['django']
+
+Usage
+-----
+
+If you run py.test in the root directory of your Django project, it will
+attempt to import the settings and run any tests. It is backwards compatible
+with Django's unittest test cases.
+
+Note that the default py.test collector is used, as well as any file within a
+tests directory. As such, so it will not honour `INSTALLED_APPS`. You must use
+`collect_ignore` in a `conftest.py` file to exclude any tests you don't want
+to be run.
+
+A `--settings` option is provided for explicitly setting a settings module,
+similar to `manage.py`.
+
+Hooks
+-----
+
+The session start/finish and setup/teardown hooks act like Django's `test`
+management command and unittest test cases. This includes creating the test
+database and maintaining a constant test environment, amongst other things.
+Additionally, it will attempt to restore the settings at the end of each test,
+so it is safe to modify settings within a test.
+
+Funcargs
+--------
+
+### `client`
+
+A Django test client instance.
+
+Example:
+
+ def test_something(client)
+ assert 'Success!' in client.get('/path/')
+
+
+### `rf`
+
+An instance of Simon Willison's excellent
+[RequestFactory](http://www.djangosnippets.org/snippets/963/).
+
+`@py.test.params` decorator
+---------------------------
+
+A decorator to make parametrised tests easy. Takes a list of dictionaries of
+keyword arguments for the function. A test is created for each dictionary.
+
+Example:
+
+ @py.test.params([dict(a=1, b=2), dict(a=3, b=3), dict(a=5, b=4)])
+ def test_equals(a, b):
+ assert a == b
4 conftest.py
@@ -0,0 +1,4 @@
+import os, sys
+sys.path.insert(0, '')
+os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
+pytest_plugins = ['django']
36 pytest_django/__init__.py
@@ -0,0 +1,36 @@
+"""
+This is just an intermediate plugin that sets up the django environment and
+loads the main plugin
+"""
+
+from django.core.management import setup_environ
+import os
+import py
+
+def pytest_addoption(parser):
+ parser.addoption('--settings', help='The Python path to a Django settings module, e.g. "myproject.settings.main". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.', default=None)
+
+def pytest_configure(config):
+ try:
+ import settings
+ except ImportError:
+ pass
+ else:
+ setup_environ(settings)
+ config_settings = config.getvalue('settings')
+ if config_settings is not None:
+ os.environ['DJANGO_SETTINGS_MODULE'] = config_settings
+ from pytest_django import plugin
+ config.pluginmanager.register(plugin)
+
+def pytest_collect_file(path, parent):
+ """
+ Load all files in a tests directory as tests, following Django convention.
+
+ However, we still load files such as test_urls.py in application
+ directories which are typically not tests. That might need manually
+ overriding in conftest files.
+ """
+ if path.check(fnmatch="tests/*.py"):
+ return parent.Module(path, parent=parent)
+
39 pytest_django/client.py
@@ -0,0 +1,39 @@
+from django.test import Client
+from django.core.handlers.wsgi import WSGIRequest
+
+class RequestFactory(Client):
+ """
+ Class that lets you create mock Request objects for use in testing.
+
+ Usage:
+
+ rf = RequestFactory()
+ get_request = rf.get('/hello/')
+ post_request = rf.post('/submit/', {'foo': 'bar'})
+
+ This class re-uses the django.test.client.Client interface, docs here:
+ http://www.djangoproject.com/documentation/testing/#the-test-client
+
+ Once you have a request object you can pass it to any view function,
+ just as if that view had been hooked up using a URLconf.
+
+ http://www.djangosnippets.org/snippets/963/
+ """
+ def request(self, **request):
+ """
+ Similar to parent class, but returns the request object as soon as it
+ has created it.
+ """
+ environ = {
+ 'HTTP_COOKIE': self.cookies,
+ 'PATH_INFO': '/',
+ 'QUERY_STRING': '',
+ 'REQUEST_METHOD': 'GET',
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': 'testserver',
+ 'SERVER_PORT': 80,
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
+ }
+ environ.update(self.defaults)
+ environ.update(request)
+ return WSGIRequest(environ)
171 pytest_django/plugin.py
@@ -0,0 +1,171 @@
+from django.conf import settings
+from django.core import mail, management
+from django.core.management import call_command
+from django.core.urlresolvers import clear_url_caches
+from django.db import connection, transaction
+from django.test.client import Client
+from django.test.testcases import disable_transaction_methods, restore_transaction_methods, TestCase
+from django.test.utils import setup_test_environment, teardown_test_environment
+from pytest_django.client import RequestFactory
+
+class DjangoManager(object):
+ """
+ A Django plugin for py.test that handles creating and destroying the
+ test environment and test database.
+
+ Similar to Django's TransactionTestCase, a transaction is started and
+ rolled back for each test. Additionally, the settings are copied before
+ each test and restored at the end of the test, so it is safe to modify
+ settings within tests.
+ """
+
+ old_database_name = None
+ _old_settings = []
+
+ def __init__(self, verbosity=0):
+ self.verbosity = verbosity
+
+ def pytest_sessionstart(self, session):
+ setup_test_environment()
+ settings.DEBUG = False
+
+ management.get_commands()
+ management._commands['syncdb'] = 'django.core'
+ if hasattr(settings, "SOUTH_TESTS_MIGRATE") and settings.SOUTH_TESTS_MIGRATE:
+ try:
+ from south.management.commands.syncdb import Command
+ except ImportError:
+ pass
+ else:
+ class MigrateAndSyncCommand(Command):
+ option_list = Command.option_list
+ for opt in option_list:
+ if "--migrate" == opt.get_opt_string():
+ opt.default = True
+ break
+ management._commands['syncdb'] = MigrateAndSyncCommand()
+
+ self.old_database_name = settings.DATABASE_NAME
+ connection.creation.create_test_db(self.verbosity)
+
+ def pytest_sessionfinish(self, session, exitstatus):
+ connection.creation.destroy_test_db(self.old_database_name, self.verbosity)
+ teardown_test_environment()
+
+ def pytest_itemstart(self, item):
+ # This lets us control the order of the setup/teardown
+ # Yuck.
+ if self._is_unittest(self._get_item_obj(item)):
+ item.setup = lambda: None
+ item.teardown = lambda: None
+
+ def pytest_runtest_setup(self, item):
+ self._old_settings = settings
+ item_obj = self._get_item_obj(item)
+
+ # This is a Django unittest TestCase
+ if self._is_unittest(item_obj):
+ item_obj.client = Client()
+ item_obj._pre_setup()
+ item_obj.setUp()
+ return
+
+ if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
+ call_command('flush', verbosity=self.verbosity, interactive=False)
+ if hasattr(item_obj, 'fixtures'):
+ # We have to use this slightly awkward syntax due to the fact
+ # that we're using *args and **kwargs together.
+ call_command('loaddata', *item_obj.fixtures, **{
+ 'verbosity': self.verbosity
+ })
+ else:
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+ disable_transaction_methods()
+
+ from django.contrib.sites.models import Site
+ Site.objects.clear_cache()
+
+ if hasattr(item_obj, 'fixtures'):
+ call_command('loaddata', *item_obj.fixtures, **{
+ 'verbosity': self.verbosity,
+ 'commit': False
+ })
+ if hasattr(item_obj, 'urls'):
+ settings.ROOT_URLCONF = item_obj.urls
+ clear_url_caches()
+
+ mail.outbox = []
+
+ def pytest_runtest_teardown(self, item):
+ item_obj = self._get_item_obj(item)
+
+ # This is a Django unittest TestCase
+ if self._is_unittest(item_obj):
+ item_obj.tearDown()
+ item_obj._post_teardown()
+ return
+
+ elif settings.DATABASE_SUPPORTS_TRANSACTIONS:
+ restore_transaction_methods()
+ transaction.rollback()
+ try:
+ transaction.leave_transaction_management()
+ except transaction.TransactionManagementError:
+ pass
+ connection.close()
+
+ for setting in dir(self._old_settings):
+ if setting == setting.upper():
+ setattr(self, setting, getattr(self._old_settings, setting))
+
+ def _get_item_obj(self, item):
+ try:
+ return item.obj.im_self
+ except AttributeError:
+ return None
+
+ def _is_unittest(self, item_obj):
+ return issubclass(type(item_obj), TestCase)
+
+def pytest_configure(config):
+ verbosity = 0
+ if config.getvalue('verbose'):
+ verbosity = 1
+ config.pluginmanager.register(DjangoManager(verbosity))
+
+def pytest_funcarg__client(request):
+ return Client()
+
+def pytest_funcarg__rf(request):
+ return RequestFactory()
+
+def pytest_namespace():
+ """
+ Sets up the py.test.params decorator.
+ """
+ def params(funcarglist):
+ """
+ A decorator to make parametrised tests easy. Takes a list of
+ dictionaries of keyword arguments for the function. A test is created
+ for each dictionary.
+
+ Example:
+
+ @py.test.params([dict(a=1, b=2), dict(a=3, b=3), dict(a=5, b=4)])
+ def test_equals(a, b):
+ assert a == b
+ """
+ def wrapper(function):
+ function.funcarglist = funcarglist
+ return function
+ return wrapper
+ return {'params': params}
+
+def pytest_generate_tests(metafunc):
+ """
+ Generates parametrised tests if the py.test.params decorator has been
+ used.
+ """
+ for funcargs in getattr(metafunc.function, 'funcarglist', ()):
+ metafunc.addcall(funcargs=funcargs)
22 setup.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from distutils.core import setup
+
+setup(
+ name='pytest_django',
+ version='0.1',
+ description='A Django plugin for py.test.',
+ author='Ben Firshman',
+ author_email='ben@firshman.co.uk',
+ url='http://github.com/bfirsh/pytest_django/',
+ packages=[
+ 'pytest_django',
+ ],
+ classifiers=['Development Status :: 4 - Beta',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Topic :: Software Development :: Testing'],
+)
0  tests/__init__.py
No changes.
0  tests/app/__init__.py
No changes.
9 tests/app/fixtures/items.json
@@ -0,0 +1,9 @@
+[
+ {
+ "pk": 1,
+ "model": "app.item",
+ "fields": {
+ "name": "Fixture item"
+ }
+ }
+]
5 tests/app/models.py
@@ -0,0 +1,5 @@
+from django.db import models
+
+class Item(models.Model):
+ name = models.CharField(max_length=100)
+
3  tests/settings.py
@@ -0,0 +1,3 @@
+DATABASE_ENGINE = 'sqlite3'
+ROOT_URLCONF = 'tests.urls'
+INSTALLED_APPS = ['tests.app']
53 tests/test_environment.py
@@ -0,0 +1,53 @@
+from django.conf import settings
+from django.core import mail
+from app.models import Item
+
+# It doesn't matter which order all the _again methods are run, we just need
+# to check the environment remains constant.
+# This is possible with some of the testdir magic, but this is a nice lazy to
+# it
+
+def test_mail():
+ assert len(mail.outbox) == 0
+ mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com'])
+ assert len(mail.outbox) == 1
+ m = mail.outbox[0]
+ assert m.subject == 'subject'
+ assert m.body == 'body'
+ assert m.from_email == 'from@example.com'
+ assert list(m.to) == ['to@example.com']
+
+def test_mail_again():
+ test_mail()
+
+def test_settings():
+ assert settings.DEFAULT_FROM_EMAIL != 'foo@example.com'
+ settings.DEFAULT_FROM_EMAIL == 'foo@example.com'
+
+def test_settings_again():
+ test_settings()
+
+def test_database_rollback():
+ assert Item.objects.count() == 0
+ Item.objects.create(name='blah')
+ assert Item.objects.count() == 1
+
+def test_database_rollback_again():
+ test_database_rollback()
+
+class TestFixtures:
+ fixtures = ['items']
+
+ def test_fixtures(self):
+ assert Item.objects.count() == 1
+ assert Item.objects.all()[0].name == 'Fixture item'
+
+ def test_fixtures_again(self):
+ """Ensure fixtures are only loaded once."""
+ self.test_fixtures()
+
+class TestUrls:
+ urls = 'tests.urls_test'
+
+ def test_urls(self, client):
+ client.get('/test_url/').content == 'Test URL works!'
31 tests/test_funcargs.py
@@ -0,0 +1,31 @@
+from django.test.client import Client
+from pytest_django.client import RequestFactory
+
+pytest_plugins = ['pytester']
+
+def test_params(testdir):
+ testdir.makeconftest("""
+ import os, sys
+ import pytest_django as plugin
+ sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(plugin.__file__), '../')))
+ os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
+ pytest_plugins = ['django']
+ """)
+ p = testdir.makepyfile("""
+ import py
+ @py.test.params([dict(arg1=1, arg2=1), dict(arg1=1, arg2=2)])
+ def test_myfunc(arg1, arg2):
+ assert arg1 == arg2
+ """)
+ result = testdir.runpytest("-v", p)
+ assert result.stdout.fnmatch_lines([
+ "*test_myfunc*0*PASS*",
+ "*test_myfunc*1*FAIL*",
+ "*1 failed, 1 passed*"
+ ])
+
+def test_client(client):
+ assert isinstance(client, Client)
+
+def test_rf(rf):
+ assert isinstance(rf, RequestFactory)
15 tests/test_requestfactory.py
@@ -0,0 +1,15 @@
+from django.http import HttpRequest
+from pytest_django.client import RequestFactory
+
+def test_get():
+ request = RequestFactory().get('/path/')
+ assert isinstance(request, HttpRequest)
+ assert request.path == '/path/'
+ assert request.method == 'GET'
+
+def test_post():
+ request = RequestFactory().post('/submit/', {'foo': 'bar'})
+ assert isinstance(request, HttpRequest)
+ assert request.path == '/submit/'
+ assert request.method == 'POST'
+ assert request.POST['foo'] == 'bar'
58 tests/test_unittest.py
@@ -0,0 +1,58 @@
+from django.test import TestCase
+from app.models import Item
+
+class TestFixtures(TestCase):
+ fixtures = ['items']
+
+ def test_fixtures(self):
+ assert Item.objects.count() == 1
+ assert Item.objects.all()[0].name == 'Fixture item'
+
+ def test_fixtures_again(self):
+ """Ensure fixtures are only loaded once."""
+ self.test_fixtures()
+
+class TestSetup(TestCase):
+ def setUp(self):
+ """setUp should be called after starting a transaction"""
+ assert Item.objects.count() == 0
+ Item.objects.create(name='Some item')
+ Item.objects.create(name='Some item again')
+
+ def test_count(self):
+ self.assertEqual(Item.objects.count(), 2)
+ assert Item.objects.count() == 2
+ Item.objects.create(name='Foo')
+ self.assertEqual(Item.objects.count(), 3)
+
+ def test_count_again(self):
+ self.test_count()
+
+ def tearDown(self):
+ """tearDown should be called before rolling back the database"""
+ assert Item.objects.count() == 3
+
+
+class TestFixturesWithSetup(TestCase):
+ fixtures = ['items']
+
+ def setUp(self):
+ assert Item.objects.count() == 1
+ Item.objects.create(name='Some item')
+
+ def test_count(self):
+ assert Item.objects.count() == 2
+ Item.objects.create(name='Some item again')
+
+ def test_count_again(self):
+ self.test_count()
+
+ def tearDown(self):
+ assert Item.objects.count() == 3
+
+
+class TestUrls(TestCase):
+ urls = 'tests.urls_test'
+
+ def test_urls(self):
+ self.client.get('/test_url/').content == 'Test URL works!'
5 tests/urls.py
@@ -0,0 +1,5 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+
+)
6 tests/urls_test.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+from django.http import HttpResponse
+
+urlpatterns = patterns('',
+ url(r'^test_url/$', lambda r: HttpResponse('Test URL works!'))
+)
Please sign in to comment.
Something went wrong with that request. Please try again.