Adding py.test to your Django project

There are many ways to test in Django, this is how you can do it with Py.test:

First pip-install:

pytest==2.5.2
pytest-django==2.6

Create a file named conftest.py in your project root (next to manage.py) with the following data:

import os
import pytest
import site


site.addsitedir(os.path.dirname(__file__))
os.environ['DJANGO_SETTINGS_MODULE'] = 'yourproject.settings_testing'


pytestmark = pytest.mark.django_db


Where yourproject should be replaced with your project name, and settings_testing with the settings file you'd like to use for testing. I prefer to use a different settings file for testing and name it settings_testing.py.

The testing_settings.py file looks like this:

from settings import *
import os

mysql_user = os.environ['mysql_user']
mysql_pwd = os.environ['mysql_pwd']

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'your-database',
        'USER': mysql_user,
        'PASSWORD': mysql_pwd,
        'HOST': '',
        'PORT': '',
        'STORAGE_ENGINE': 'INNODB',
        'OPTIONS': {
            'init_command': 'SET storage_engine=INNODB',
        },
    }
}

Note that these settings are already quite specific for a project that uses a MySQL database (InnoDB) and has it's username and password in the environment. I do this because I don't like to put sensitive data in the git repository. My continious integration server sets the MySQL username and password like so:

$ export mysql_user=username
$ export mysql_pwd=password

To run the tests:

$ py.test

 

Now forget json-fixtures. With py.test we can create dynamic Python fixtures that have a scope of a testfunction.
Together with factory-boy you can create very powerful fixtures.

Go ahead, install factory-boy:

$ pip install factory-boy==2.3.1

Now create a factory for your User model that lives in the app accounts like so in /accounts/tests/factories.py:

from project.fields import COUNTRIES
from accounts import choices
from accounts import models

import factory


TEST_PASSWORD = 'test'


class AccountFactory(factory.django.DjangoModelFactory):
    FACTORY_FOR = models.Account

    first_name = factory.Sequence(lambda n: 'Firstname {0}'.format(n))
    last_name = factory.Sequence(lambda n: 'Lastname {0}'.format(n))
    email = factory.LazyAttribute(lambda obj: '{0}.{1}@example.com'.format(obj.first_name.lower().replace(' ', '_'), obj.last_name.lower().replace(' ', '_')))
    country = factory.Sequence(lambda n: COUNTRIES[n][0])
password = factory.PostGenerationMethodCall('set_password', TEST_PASSWORD)


class UserAccountFactory(AccountFactory):
    access_level = choices.ACCESS_LEVEL_USER


class AnalystAccountFactory(AccountFactory):
    access_level = choices.ACCESS_LEVEL_ANALYST
    country = None


class AdminAccountFactory(AccountFactory):
    access_level = choices.ACCESS_LEVEL_ADMIN
    country = None

Add an account fixture in conftest.py like so:

from accounts import choices as account_choices
from accounts import models as account_models
from accounts.tests import factories as account_facs


pytestmark = pytest.mark.django_db


@pytest.fixture(scope='function')
def accounts(request):
AccountObj = type('AccountObj', (object,), {})
accounts = AccountObj()

for i in range(10):
if i in [0, 1, 2]:
# create an admin
account_facs.AdminAccountFactory()
elif i in [3, 4, 5]:
# create an analist
account_facs.AnalystAccountFactory()
else:
# create a normal user
account_facs.UserAccountFactory()

accounts.admins = account_models.Account.objects.filter(access_level=account_choices.ACCESS_LEVEL_ADMIN)
accounts.analysts = account_models.Account.objects.filter(access_level=account_choices.ACCESS_LEVEL_ANALYST)
accounts.normal_users = account_models.Account.objects.filter(access_level=account_choices.ACCESS_LEVEL_USER)

return accounts

Now you can use the fixtures in your tests like so (e.g. in /accounts/tests/test_views.py):

import pytest
from django.core.urlresolvers import reverse
from accounts.tests.factories import TEST_PASSWORD
from project.tests.utils import get_redirect_url


pytestmark = pytest.mark.django_db(transaction=True)


def test_auth_login(client, accounts):
    """
    Test if login url is accessible
    """

    url = reverse('accounts:login')

    # anoymous user
    response = client.get(url)
    assert response.status_code == 200

    # admin user
    admin_user = accounts.admins.first()
    client.login(username=admin_user.get_username(), password=TEST_PASSWORD)
    response = client.get(url)
    assert response.status_code == 302
    assert get_redirect_url(response) == '/'

    # analyst user
    analyst_user = accounts.analysts.first()
    client.login(username=analyst_user.get_username(), password=TEST_PASSWORD)
    response = client.get(url)
    assert response.status_code == 302
    assert get_redirect_url(response) == '/'

    # normal user
    normal_user = accounts.normal_users.first()
    client.login(username=normal_user.get_username(), password=TEST_PASSWORD)
    response = client.get(url)
    assert response.status_code == 302
    assert get_redirect_url(response) == '/'

Where the contents of project/tests/utils.py looks like this:

def get_redirect_url(response):
return response['Location'].split('http://testserver')[1]


def get_login_url(next=None):
login_url = '/login/'
if next:
return '{0}?next={1}'.format(login_url, next)
return login_url