Two-Faced Django Part 2: Models and Django Testing
In the [first part][partone] of this tutorial we put together our [files and folder configuration][polling1]. Now we are going to start putting together the core of functionality and data that represents the core of our project. One of the keys for our project being quick to build, and easy to test, is to implement all of our functionality only once, even though it will be used differently by both the Facebook and web apps. In the best case, which happens to be the one we're aiming for, all the code we'll write in the fb and web apps will just be a simple interface layer for our core functionality in the core app.
Okay. So a lot of silly words got thrown out in the last paragraph, now lets see what I am talking about.
Mapping functionality onto models
So, we want to start writing our models, but first we need to narrow down what our program will actually do. I've said that it will be a polling program, what does a simple polling application do?
- Make polls.
- Vote on polls.
- Make users who vote on polls.
- Delete polls if their creator wants to.
Sounds simple enough. Lets put together some models that can handle that:
from django.db import models
from django.contrib.auth.models import User as DjangoUser
class Poll(models.Model):
question = models.CharField(unique=True, max_length=200)
creator = models.ForeignKey('User')
up_votes = models.IntegerField()
down_votes = models.IntegerField()
class User(models.Model):
user = models.ForeignKey(DjangoUser, blank=True, null=True)
facebook_id = models.IntegerField(blank=True, null=True)
name = models.CharField(max_length=200)
Looking at our Poll class, we can see that we are going to keep track of a question, the User who created the Poll, and the number of up and down votes it has received.
The User class is made slightly complex because we want to make the app oblivious of what interface (either the web or the fb apps in our project) is being used. Thus we have two potential ways of identifying a person with a User, either by their Facebook User ID, or by a django.contrib.auth.models.User instance.
Question Time: Do we really need to differentiate the Facebook users from webapp users?
Yes, unfortunately, we do. When we talk with a Facebook user, we are actually talking by means of an intermediary (Facebook), and have no direct connection, and thus we don't have the ability to recognize users by using cookies, and in general can't take advantage of the Django sessions or auth framework while dealing with Facebook users.
End of Question Time
Okay, so we have our models, but lets flesh them out a bit by making them accessible to the Django admin, and add a unicode method so that Django knows how to represent them. (And add some comments, just because we're professionals here, right?) This leaves us with:
from django.db import models
from django.contrib.auth.models import User as DjangoUser
class Poll(models.Model):
"""
Model used for tracking polls and responses to the poll.
"""
question = models.CharField(unique=True, max_length=200)
creator = models.ForeignKey('User')
up_votes = models.IntegerField()
down_votes = models.IntegerField()
class Admin:
pass
def unicode(self):
return "%s asked by %s" % (self.question, self.creator)
class User(models.Model):
"""
Model used for associating Facebook User IDs or
django.contrib.auth.models.User instances with
actions performed on our system.
Only one of 'user' and 'facebook_id' should have a
non-null value. Facebook users will not have an
associated User, and regular web users will not
have a 'facebook_id'.
"""
user = models.ForeignKey(DjangoUser, blank=True, null=True)
facebook_id = models.IntegerField(blank=True, null=True)
name = models.CharField(max_length=200)
class Admin:
pass
def unicode(self):
return self.name
And those are our models. Good work everyone. (Note that a [zip file with all the work done in this tutorial][polling2] is available.)
Making sure it all works
Now, this is where a lesser tutorial or a lesser tutorial reader might dance into the increasingly complicated horizon. But not us, fair reader, not us. We are going to stop and take a gander at the Django testing framework and 'leverage' it to...
What the hell am I saying. We're going to write some tests.
In the Django testing framework, tests related to a specific app go in the app's folder itself, in a file named tests.py. Lets go ahead and make that (starting from our polling project folder):
emacs core/tests.py
Now lets put in some scaffolding for our tests.py file:
from django.utils import simplejson
from polling.core.models import Poll, User
MODELS = [Poll, User]
class CoreModelTest(unittest.TestCase):
"Test the models contained in the 'core' app."
def setUp(self):
'Populate test database with model instances.'
self.client = Client()
def tearDown(self):
'Depopulate created model instances from test database.'
for model in MODELS:
for obj in model.objects.all():
obj.delete()
def test_something(self):
"This is a test for something."
self.assertEquals(True, 1)
The setUp method is called before each test in a TestCase is called. Thus before every unittest (like test_function in the scaffolding above), the setUp function will be called. In the same way, after each unittest, tearDown will be called.
The tearDown method may look a little bit uneccessary. Why do we need to be so damn insistent about deleting all the models from the database after each test? Well, although there is a test database created for the Django tests to run in, it is created for each running of the test framework, and is not recreated after each individual unittest. Thus we can have situations where we are uncertain about the number and type of models in the test database at a given point. That is not particularly condusive to testing, and so I prefer to delete all previous models from the testing database after each unittest. Since we are using SQLite, this turns out to be a very quick operation, because the testing database is created in memory and thus has very quick access time (accompanied by very little permanence).
Now lets run our scaffolding.
python manage.py test
And it'll run our tests. Thats all there is to it. The bottom of the output text should look something like this:
...
----------------------------------------------------------------------
Ran 3 tests in 0.027s
OK
Destroying test database…
The three periods above the line of "------" each represent a passing test. When a test fails, the testrunner isn't afraid of letting you know. But lets verify that anyway. In the core/tests.py file, switch
self.assertEquals(True, 1)
to
self.assertEquals(True, 0)
Now run the tests again
python manage.py test
And this time our output looks like this:
..F
======================================================================
FAIL: This is a test for something.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/will/django/polling/../polling/core/tests.py", line 28, in test_something
self.assertEquals(True, 0)
AssertionError: True != 0
Ran 3 tests in 0.027s
FAILED (failures=1)
Destroying test database…
Okay. Now that we've gotten our test scaffolding together, lets write a better setUp method to actually populate the test database with some data.
Replace the old setUp method with this:
def setUp(self):
'Populate test database with model instances.'
self.client = Client()
wl = User(name="Will Larson")
wl.save()
jb = User(name="Jack Bauer")
jb.save()
bc = User(name="Bill Clinton")
bc.save()
Poll.objects.create(question="Did you vote for me?", creator=bc)
Poll.objects.create(question="Am I human?", creator=jb)
Poll.objects.create(question="Are you still reading?", creator=wl)
Now lets run our tests and make sure the setUp function works.
python manage.py test
Okay, so that didn't work out that well. It should have thrown something like:
IntegrityError: core_poll.up_votes may not be NULL
Oops. We need to be initializing a value for the *up_vote and down_vote* fields in our Poll model! One of the things about writing tests is that it really forces us to figure out what the hell we are doing, and earlier rather than later. That is a Good Thing.
Fixing up the Poll model right quick
To initialize the values in the two fields in our Poll we are going to override the save function. This is a handy technique because it lets you dynamically determine values at time of creation, and also to sanitize incoming values if you need to. (Its worth pointing out that in this specific situation it would be more appropriate to use the default argument key for the field, to the tune of "up_vote = IntegerField(default=0)", but that would miss out on an opportunity for looking at the always handy technique of overriding save.)
So, before the unicode method in our Poll object in polling/core/models.py insert this code:
def save(self):
"""Overriding save() to innitialize values for up_votes and down_votes."""
if not self.up_votes:
self.up_votes = 0
if not self.down_votes:
self.down_votes = 0
return super(Poll, self).save()
Reading through the code, if we fail to specify either the *up_vote or down_vote* fields in a Poll instance, then the save method will specify them for us before it gets to the default model save method (model is Poll's super class).
Lets save our polling/core/models.py file, and rerun our tests.
python manage.py test
And, dum de dum, everything works again.
Continuing the code-test cycle
Okay, so we have two more short implement-test cycles left in part two of this tutorial. I promise you can take a break soon.
First I want to create a couple of nifty methods for Poll to take care of voting the poll up or down, and also a function for calculating the current overall score for a poll. They're going to look like this:
def score(self):
return self.up_votes - self.down_votes
def up(self):
self.up_votes = self.up_votes + 1
self.save()
def down(self):
self.down_votes = self.down_votes + 1
self.save()
Now lets right some tests to make sure that they work correctly. Opening back up polling/core/test.py:
def test_poll(self):
"This is a test for something."
poll = Poll.objects.get(pk=1)
up = poll.up_votes
down = poll.down_votes
self.assertEquals(up, poll.up_votes)
self.assertEquals(down, poll.up_votes)
self.assertEquals(up-down, poll.score())
poll.up()
up = up + 1
self.assertEquals(up, poll.up_votes)
poll.down()
down = down + 1
self.assertEquals(down, poll.down_votes)
self.assertEquals(up-down, poll.score())
That should be a method of the CoreModelTest class. We retrieve a poll we have already created (it was created in the *setUp method of CoreModelTest), and then retrieve its initial values for up_votes and down_votes*. Then we have a few sanity checks, and then go about calling the up, down and score methods and verifying they are returning the values we expect.
So lets run our tests and see what happens:
python manage.py test
And they pass. Excellent.
One more quick cycle
Okay, now we want to add a wee bit of functionality to the User class to facilitate creating Poll instances. We want to make a create_poll method that creates a new Poll instance, created by the User instance whose method was called. Its going to look like this:
def create_poll(self, question):
p = Poll(creator=self, question=question)
p.save()
return p
And a test to make sure it all works:
def test_user(self):
"This is a test for the polling.core.model.User model."
user = User.objects.get(pk=1)
p = user.create_poll("Is this actually working?")
self.assertEquals(user, p.creator)
And... you know the drill... run the test:
python manage.py test
And the tests all pass. Hallelujah.
Another exciting conclusion.
Okay. So we've worked through creating our models for our application, and have also introduced tests to our app, so we have some kind of proof that what we're doing makes some sense. You can download the snapshot of this project (what it looks like having completed this tutorial) [here][polling2].
When you're ready to move on, part three is waiting for you.