From fixtures to factories: Updating testing code from manual data fixtures to Fixture Boy

In part one of my Django authentication/testing tutorial, the testing code originally contained a manually-created fixture of test users, including a function to directly save one to the database.

TEST_USER_GIBBERISH = {
    "username": "3u9osuhceo",  "password":  "8um48eut93te",
    "first_name": "xrc4b46id", "last_name": "agc4598pdb9uyfn",
    "birth_year": 1931,        "email":     "oaudetnnao@oth09867.com"}
TEST_USER_KERMIT = {
    "username":   "kermit",    "password":  "timrek",
    "first_name": "Kermit",    "last_name": "The Frog",
    "birth_year": 1955,        "email":     "kermit@muppets.com"}
TEST_USER_FOZZIE = {
    "username":   "fozzie",    "password":  "eizzof",
    "first_name": "Fozzie",    "last_name": "Bear",
    "birth_year": 1976,        "email":     "fozzie@muppets.com"}
TEST_USERS = [TEST_USER_GIBBERISH, TEST_USER_KERMIT, TEST_USER_FOZZIE]

For the purpose of the authentication tutorial, manual fixtures are just fine. In the real world, they should be avoided. As described in Carl Meyer’s excellent talk on Django testing, from PYCON 2012, at 16:20, manually-created fixtures are difficult to maintain, slow to load, and dangerous to share among tests.

Instead, model factories should be used. I’m using Factory Boy. The main benefits of model factories are:

  • When the model changes, the factory automatically detects this, always returning a properly formatted object.
  • Although you must specify defaults for each field, in the definition of each factory, individual tests do not need to provide specific values, expect for only those attributes requiring special values in order to pass. See the top-most example on the main Factory Boy documentation page.
  • Objects can be easily created in memory only, or saved to the database, as required by your tests.

The question is moot.

In addition to fixtures, I also changed it so the user objects are no longer held locally; they are now only created in the database. I thought it was important to have local copies of these objects, so the database values could be compared against them. It’s not (and doing so created problems because the local object was static)-trust Django and Factory Boy to insert them properly. This therefore renders the model tests moot, so the entire ModelsTestCase class has been eliminated, and the file has therefore been renamed to `test__utilities.py`

Below is the original, manual-fixture file in its entirety, test__utilities.py. The updated, Fixture Boy version, is now in part one of the tutorial.

"""
Tests for models.py.

DEPENDS ON TEST:  *nothing* (must not depend on any test_*.py file)
DEPENDED ON TEST: test__profile.py

To run these tests:
  1. source /home/myname/django_files/django_auth_lifecycle_venv/bin/activate
  2. cd /home/myname/django_files/django_auth_lifecycle/
  3. python -Wall manage.py test auth_lifecycle.test__utilities

Running tests documentation:
- https://docs.djangoproject.com/en/1.7/topics/testing/overview/#running-tests

Information on '-Wall' is at the bottom of this section in the official
Django documentation:
- https://docs.djangoproject.com/en/1.7/topics/testing/overview/#running-tests

If the output is too verbose, try it again without '-Wall'.

If a test fails because the test database cannot be created, grant your
database user creation privileges:
- http://dba.stackexchange.com/questions/33285/how-to-i-grant-a-user-account-permission-to-create-databases-in-postgresql

pylint auth_lifecycle.test__utilities > pylint_output.txt
pylint auth_lifecycle.test__view_birth_stats > pylint_output.txt
pylint auth_lifecycle.test__view_user_profile > pylint_output.txt
"""
from django.test                import TestCase
from auth_lifecycle.models      import UserProfile
from django.contrib.auth.models import User
from .models                    import MIN_BIRTH_YEAR

TEST_USER_GIBBERISH = {
    "username": "3u9osuhceo",  "password":  "8um48eut93te",
    "first_name": "xrc4b46id", "last_name": "agc4598pdb9uyfn",
    "birth_year": 1931,        "email":     "oaudetnnao@oth09867.com"}
"""
The main test user used by all tests, containing only gibberish values.

The reason for the gibberish values is to easily and confidently
extract them from the webpage, without having to somehow
delineate them, such as with marker comments like

<!-- UNITRQD-start: email -->email@something.com<!-- UNITRQD-end -->

See this comment, and the entire question-post it's part of:
- http://codereview.stackexchange.com/q/66898/35364#comment121964_66917
"""
TEST_USER_KERMIT = {
    "username":   "kermit",    "password":  "timrek",
    "first_name": "Kermit",    "last_name": "The Frog",
    "birth_year": 1955,        "email":     "kermit@muppets.com"}
TEST_USER_FOZZIE = {
    "username":   "fozzie",    "password":  "eizzof",
    "first_name": "Fozzie",    "last_name": "Bear",
    "birth_year": 1976,        "email":     "fozzie@muppets.com"}
TEST_USERS = [TEST_USER_GIBBERISH, TEST_USER_KERMIT, TEST_USER_FOZZIE]
"""
An array of users for testing purposes only. Each element is a dictionary
containing all non-id attributes in both the User and UserProfile models.

Equal to `[TEST_USER_GIBBERISH, TEST_USER_KERMIT, TEST_USER_FOZZIE]`

Creating the test users in this way allows us to centralize their
attributes, so we don't have hard-coded passwords in multiple places
throughout the testing code, for example.

User model attributes:

https://docs.djangoproject.com/en/1.7/topics/auth/default/#user-objects
"""
def _insert_test_user(test_user):
    """
    Insert and save a single test user.

    Private function for this file only.
    """
    user = User.objects.create_user(
        username=test_user['username'], password=test_user['password'],
        first_name=test_user['first_name'],
        last_name=test_user['last_name'], email=test_user['email'])

    """
    Creating the user with
        user = User(...)
        user.save()
    does not properly hash and salt the password. Although it does save it
    to the database, attempting
        self.client.login(username='theusername', password='thepassword')
    fails (returns False)

    https://docs.djangoproject.com/en/1.7/ref/contrib/auth/#django.contrib.auth.models.UserManager.create_user
    http://stackoverflow.com/questions/26306424/cant-login-a-just-created-user-in-a-django-test
    """

    #The user's id is automatically created by the database. To function
    #as the foreign key, it must be duplicated to the profile.

    profile = UserProfile(user_id=user.id, birth_year=test_user['birth_year'])
    profile.save()

def create_insert_test_users():
    """Insert all <link to TEST_USERS> into the test-database."""
    for  test_user in TEST_USERS:
        #print(test_user)
        _insert_test_user(test_user)

class ModelsTestCase(TestCase):
    """Tests for models.py."""
    def setUp(self_ignored):
        """Insert test users."""
        create_insert_test_users()

    def test_all_demo_users_inserted(self):
        """
        The birth year in the database should equal the local TEST_USERS
        dictionary value.
        """
        for  test_user in TEST_USERS:
            user = User.objects.get(username=test_user['username'])
            self.assertEqual(test_user['birth_year'], user.profile.birth_year)
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s