Let’s TDD a Simple App in Django
In this tutorial, I will present an end-to-end example of a simple application – made strictly with TDD in Django. I will walk you through each step, one at a time, while explaining the decisions I made in order to get the task done. The example closely follows the rules of TDD: write tests, write code, refactor.
Introduction to TDD and Unittests
TDD is a “test-first” technique to develop and design software. It is almost always used in agile teams, being one of the core tools of agile software development. TDD was first defined and introduced to the professional community by Kent Beck in 2002. Since then, it has become an accepted – and recommended – technique in everyday programming.
TDD has three core rules:
- You are not allowed to write any production code, if there is not a failing test to warrant it.
- You are not allowed to write more of a unit test than is strictly necessary to make it fail. Not compiling / running is failing.
- You are not allowed to write more production code than is strictly necessary to make the failing test pass.
The unittest module provides a rich set of tools for constructing and running tests.
The main methods that we make use of in unit testing for Python are:
- assert – base assert allowing you to write your own assertions
- assertEqual(a, b) – check a and b are equal
- assertNotEqual(a, b) – check a and b are not equal
- assertIn(a, b) – check that a is in the item b
- assertNotIn(a, b) – check that a is not in the item b
- assertFalse(a) – check that the value of a is False
- assertTrue(a) – check the value of a is True
- assertIsInstance(a, TYPE) – check that a is of type “TYPE”
- assertRaises(ERROR, a, args) – check that when a is called with args that it raises ERROR
There are certainly more methods available to us, which you can view – Python Unit Test Doc’s – but, in my experience, the ones listed above are among the most frequently used.
Starting the Project and Creating the First Test
We are going to use the latest version of Django (1.6) which supports Python 2.7 to 3.3. Before proceeding, make sure that you have the apt version by executing python -v in the terminal. Note that Python 2.7.5 is preferred. All throughout this tutorial, we’ll use pip as our package manager and virtualenv to set up the Virtual Environments. To install these, fire up the terminal and execute the following commands as root
|
1 2 3 |
curl http://python-distribute.org/distribute_setup.py | sudo python curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | sudo python sudo pip install virtualenv |
To set up our Django development environment, we’ll start off by creating a Virtual Environment. Execute the following commands in the terminal (preferably inside your development directory)
|
1 2 |
virtualenv --no-site-packages tdd_env source tdd_env/bin/activate |
With our Virtual Environment, set up and activated (your command prompt should be changed to reflect the environment’s name), let’s move on to installing the dependencies for the project. Apart from Django, we’ll be using South to handle the database migrations. We’ll use pip to install both of them. Do note that from here on, we’ll be doing everything inside the virtualenv. As such, ensure that it’s activated before proceeding.
|
1 |
pip install Django South |
With all of the dependencies set up, we’re ready to move on to creating a new Django Project.
We’ll begin by creating a new Django project and our app. cd into your preferred directory and run:
|
1 2 3 |
django-admin.py startproject tdd cd tdd django-admin.py startapp app |
There should be a main folder for source classes, and a Tests/ file, naturally, for the tests.
Lets write our first test
|
1 2 3 4 5 6 7 |
from django.test import TestCase from app.models import Articles class ArticleModelTest(TestCase): def test_creating_a_new_article(self): article = Articles() print article |
We have imported the TestCase from the Django tests which makes use of the Python’s UnitTest class.
Remember! We are not allowed to write any production code before a failing test – not even a class declaration! That’s why I wrote the first simple test above, called test_creating_a_new_article. I consider it to be a nice opportunity to think about the model we are going to create. Do we need a model? What should we call it? Should it be a simple one or should it have many foreign keys?
You can run the tests like this from your terminal
|
1 |
python manage.py test app |
When you run the test above, you will receive a Import Error message, like the following:
|
1 |
ImportError: cannot import name Articles |
Yikes! We should do something about it. Create an empty Article model in the project’s models.py.
|
1 2 3 4 |
from django.db import models class Articles(models.Model): pass |
That’s it. If you run the test again, it passes. Congratulations on your first test!
|
1 2 3 4 |
Ran 1 test in 0.001s OK Destroying test database for alias 'default'... |
The First Real Test
So we have our project set up and running; now we need to think about our first real test.
What would be the simplest…the dumbest…the most basic test that would make our current production code fail? Well, the first thing that comes to mind is “Lets create the models for our app, and expect the result to be saved in the database” This sounds doable; let’s write the test.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from django.test import TestCase from app.models import Articles class ArticleModelTest(TestCase): def test_creating_a_new_article(self): article = Articles() article.title = "This is title" article.body = "This is body" article.save() articles_in_database = Articles.objects.all() self.assertEquals(len(articles_in_database), 1) only_article_in_database = articles_in_database[0] self.assertEquals(only_article_in_database, article) self.assertEquals(only_article_in_database.title, "This is title") self.assertEquals(only_article_in_database.body, "This is body") |
Note: All tests names should start with the word test.
Now, run the tests again and see what fails it now. The failing result is as shown
|
1 |
OperationalError: no such table: app_articles |
What we are doing here, we are creating an object of our article class and assigning the title and body variables to the object and saving it in database. Then we are querying all the articles from the database using Article.objects.all() which will certainly return the first article we just created, then we are checking the various conditions if the article we created is the only article present in the database or already that database has some articles.
Now to make our test pass, run the command syncdb to create the database. The terminal will prompt you for creating a superuser, enter the details and then you are done with database but wait you haven’t migrated your Article model yet. To do so, run this command in terminal.
|
1 |
python manage.py schemamigration app --initial |
Terminal will output something like this
|
1 2 |
+ Added model app.Articles Created 0001_initial.py. You can now apply this migration with: ./manage.py migrate app |
Now run migrate command to migrate the model schema.
|
1 |
python manage.py migrate app |
Now again rerun your test and you again get an failing test
|
1 |
AttributeError: 'Articles' object has no attribute 'title' |
Add the title attribute to your models and again run migrations commands to ship the title attrribute to the Article table in the database.
|
1 2 3 4 |
import random, string class Articles(models.Model): title = models.CharField(max_length=140,default=random.choice(string.lowercase)) |
Now run the migration command.
|
1 |
python manage.py schemamigration app --auto |
This time we have to pass --auto as the command line arguement which will pick up the changes in model automatically.
|
1 2 3 4 |
import random, string class Articles(models.Model): title = models.CharField(max_length=140,default=random.choice(string.lowercase)) |
On successful migration, running tests again won’t shout for old AttributeError but it will shout for the another new AttributeError, which was obvious because we haven’t yet declared body attribute in our Article model, add body attribute to Article model.
Now our model looks something like this
|
1 2 3 4 5 6 7 |
import random, string from django.core.urlresolvers import reverse from django.db import models class Articles(models.Model): title = models.CharField(max_length=140,unique=True, default=random.choice(string.lowercase)) body = models.TextField(default=random.choice(string.lowercase)) |
Dont forget to apply migrations whenever you modify your models. We are using the default values in the models to escape from the unwanted errors from south.
Now run the tests again and this time our tests pass. (If you are using IDE, green light will glow for test pass and red for test fail. )

Wow! That was easy, wasn’t it?
We have missed out refactoring. This is an integral part of TDD. If you just stop when all your tests pass, you are going to have test that works, but isn’t maintainable. You need to refactor your working solution into a more maintainable, and robust solution, since we are having a really small model, it’s not possible to refactor it more, so we are leaving it here, otherwise you need to refactor your code and run the tests again to check if the new changes breaks anything and if nothing breaks, you are good to go else refactor the code until the tests pass.
Don’t be afraid of lengthy variable names for your tests; auto-completion is your friend! It’s better to be as descriptive as possible.
Adding tests for creating the articles
What we have to do now? We have to first register our model with the app, login to django admin and create new articles. Ok, so lets start from the url for admin page first. We know admin resides in /admin/ url, but is that really true, lets check it by writing a test for it.
|
1 2 3 |
def test_admin_url_test(self): response = self.client.get('/admin/') self.assertEqual(response.status_code, 200) |
The test itself is simple. We ask the Client (self.client) built-in to Django’s TestCase to fetch the URL /admin/ using GET. We store that response (an HttpResponse in response) then perform tests on it.
We get the failing test saying that 400 != 200, because we haven’t yet enabled the admin and haven’t defined the url for admin.
|
1 |
AssertionError: 404 != 200 |
To make it pass, add 'django.contrib.admin', in your settings.py and define the admin urls in project’s url.py file.
|
1 2 3 4 5 6 7 8 |
from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', url(r'^admin/', include(admin.site.urls)) ) |
Sync the database and rerun the tests and you can see that this time both of our tests pass.
In this case, we do a simple check on what status code did we get back. Since successful HTTP GET requests result in a 200, we do an assertEqual to make sure response.status_code = 200. When we run our tests, we get:
|
1 2 3 4 |
Ran 2 tests in 0.045s OK Destroying test database for alias 'default'... |
Now, we know that admin urls are smelling good and so lets move forward and login to our admin and create some articles through tests.
We are going to need selenium for doing this tests, Selenium automates browsers and checks the condition we pass to selenium in our tests.
Note: You need to configure chromedriver before you can run tests with the browser.
Download chromedriver and place it inside your virtualenv’s script folder (if using windows) and then run the tests.
If you have correctly installed chromedriver, check it by typing chromedriver and it should show something like this
|
1 |
Starting ChromeDriver (v2.7.236900) on port 9515 |
Now lets write the tests for it in the same tests.py.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
from django.test import LiveServerTestCase from selenium import webdriver from selenium.webdriver.common.keys import Keys class ArticleAdminTest(LiveServerTestCase): def setUp(self): self.browser = webdriver.Chrome() self.browser.implicitly_wait(3) def tearDown(self): self.browser.quit() def test_can_create_new_articles(self): self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text) username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text) article_links = self.browser.find_elements_by_link_text('Articless') |
- find_elements_by_name which is most useful for form input fields, which it locates by using the name="xyz" HTML attribute
- send_keys - which sends keystrokes, as if the user was typing something (notice also the Keys.RETURN, which sends an enter key there are lots of other options inside Keys, like tabs, modifier keys etc
- find_elements_by_link_text - notice the s on elements; this method returns a list of WebElements.
What our tests do, they open up the new virtual browser and goes to the admin area in the given url self.browser.get(self.live_server_url + ‘/admin/’) and check if the body tag has the keyword Django Administration and if its present, login using the credentials. i have created the super user with the username as admin and password as admin, you have to provide what credentials you gave when you were creating a super user and if the browser successfully logs in, find for the text, site administration in the body and relatively find the articles link too.
Now run the tests, something magical happens, a quick little browser opens up and enters username and password and it closes and you can in the terminal log that your test has failed. You get this AssertionError again
|
1 |
AssertionError: 'Site administration' not found in u'Django administration\nPlease enter the correct username and password for a staff account. Note that both fields may be case-sensitive.\nUsername:\nPassword:\n ' |
The username and password didn’t work – you might think that’s strange, because we literally just set them up during the syncdb, but the reason is that the Django test runner actually creates a separate database to run tests against – this saves your test runs from interfering with production data.
So we need a way to set up an admin user account in the test database. Thankfully, Django has the concept of fixtures, which are a way of loading data into the database from text files.
We can save the admin account using the django dumpdata command, and put them into a folder called fixtures in our app and run this command:
|
1 |
python manage.py dumpdata auth.User > app/fixtures/admin.json |
Now add this fixture in your ArticleAdminTest.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class ArticleAdminTest(LiveServerTestCase): fixtures = ['admin.json'] def setUp(self): self.browser = webdriver.Chrome() self.browser.implicitly_wait(3) def tearDown(self): self.browser.quit() def test_can_create_new_articles(self): self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text) username_field = self.browser.find_element_by_name('username') username_field.send_keys('admin') password_field = self.browser.find_element_by_name('password') password_field.send_keys('admin') password_field.send_keys(Keys.RETURN) body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text) article_links = self.browser.find_element_by_link_text('Articless') |
You can get the failing test again with this AssertionError
|
1 2 |
self.assertEquals(len(article_links), 1) AssertionError: 0 != 1 |
Create admin.py inside your app folder and register our small tested model.
|
1 2 3 4 |
from django.contrib import admin from app.models import Articles admin.site.register(Articles) |
Now run the tests again and you will see that our tests pass.
|
1 2 3 4 |
Ran 3 tests in 9.208s OK Destroying test database for alias 'default'... |
Hooray! So far so good.
Now lets create articles automatically by the browser and lets see if our test pass, Add this tests below the existing tests.
|
1 2 3 4 5 6 7 |
self.browser.get(self.live_server_url + '/admin/app/articles/add') title_field = self.browser.find_element_by_name('title') title_field.send_keys('Tis is New Title Post') body_field = self.browser.find_element_by_name('body') body_field.send_keys('Tis is body') body_field.send_keys(Keys.ENTER) |
Run the tests again and you can see that all the tests still passes.
Lets add more fields in our model, to do so, first add tests for that fields and then add them in models when tests fail. modifying the first TestCase test_creating_a_new_article, we have added new fields.
|
1 2 3 |
def test_creating_a_new_article(self): article = Articles(title="This is title", body="This is body", slug="this is slug", created=timezone.now()) article.save() |
if you run the tests, it will fail, so lets add those fields in models and then run the test.
|
1 2 3 4 5 |
class Articles(models.Model): title = models.CharField(max_length=140,unique=True, default=random.choice(string.lowercase)) body = models.TextField(default=random.choice(string.lowercase)) slug = models.SlugField(unique=True, max_length=255,default=random.choice(string.lowercase)) created = models.DateTimeField(default=datetime.datetime.now) |
Our Tests pass now, so lets add some code and again check if something is broken or not.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Articles(models.Model): title = models.CharField(max_length=140,unique=True, default=random.choice(string.lowercase)) body = models.TextField(default=random.choice(string.lowercase)) slug = models.SlugField(unique=True, max_length=255,default=random.choice(string.lowercase)) created = models.DateTimeField(default=datetime.datetime.now) class Meta: ordering = ['-created'] def __unicode__(self): return self.title def get_absolute_url(self): return reverse('app.views.post', args=[self.slug]) |
Well nothing broke and still all of our tests has passed. Congratulations again for completing one full cycle of TDD.
Lets move on and create an article by going to admin area. Now we will write the tests for index view.
|
1 2 3 4 |
def test_for_index_view(self): self.browser.get(self.live_server_url + '/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Articles', body.text) |
Now run the tests and watch our last test fail.
|
1 |
AssertionError: 'Articles' not found in u'Server Error (500)' |
Lets fix that, open up your views.py and write this code
|
1 2 3 4 5 6 |
from django.shortcuts import render from app.models import Articles def index(request): articles = Articles.objects.all() return render(request, 'index.html', {'articles':articles}) |
Now we get another AssertionError stating that URL is not found on ‘/’
|
1 |
AssertionError: 'Articles' not found in u'Not Found\nThe requested URL / was not found on this server.' |
So, our assumption now is we have to define the url’s for index.
Open urls.py and add this line
|
1 |
url(r'^$', 'app.views.index'), |
And this time, our tests passed and now we can see our articles in the index page.
With that we have created a small app in Django using TDD approach.
tests.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
from django.test import LiveServerTestCase from selenium import webdriver from selenium.webdriver.common.keys import Keys from django.test import TestCase from app.models import Articles from django.utils import timezone class ArticleModelTest(TestCase): def test_creating_a_new_article(self): article = Articles(title="This is title", body="This is body", slug="this is slug", created=timezone.now()) article.save() articles_in_database = Articles.objects.all() self.assertEquals(len(articles_in_database), 1) only_article_in_database = articles_in_database[0] self.assertEquals(only_article_in_database, article) self.assertEquals(only_article_in_database.title, "This is title") self.assertEquals(only_article_in_database.body, "This is body") self.assertEqual(only_article_in_database.slug,"this is slug") def test_admin_url_test(self): response = self.client.get('/admin/') self.assertEqual(response.status_code, 200) class ArticleAdminTest(LiveServerTestCase): fixtures = ['admin.json'] def setUp(self): self.browser = webdriver.Chrome() self.browser.implicitly_wait(3) def tearDown(self): self.browser.quit() def test_can_create_new_articles(self): self.browser.get(self.live_server_url + '/admin/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Django administration', body.text) username_field = self.browser.find_element_by_name('username') username_field.send_keys('ajay') password_field = self.browser.find_element_by_name('password') password_field.send_keys('12345') password_field.send_keys(Keys.RETURN) body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text) self.browser.find_element_by_link_text('Articless').click() self.browser.find_element_by_link_text('Add articles').click() title_field = self.browser.find_element_by_name('title') title_field.send_keys('Tis is New Title Post') body_field = self.browser.find_element_by_name('body') body_field.send_keys('Tis is body') body_field.send_keys(Keys.RETURN) def test_for_index_view(self): self.browser.get(self.live_server_url + '/') body = self.browser.find_element_by_tag_name('body') self.assertIn('Articles', body.text) |
This is just the overview of how TDD is done in real situations and few words on stopping and “being done.” If you use TDD, you force yourself to think about all sorts of situations. You then write tests for those situations, and, in the process, begin to understand the problem much better. Usually, this process results in an intimate knowledge of the algorithm. If you can’t think of any other failing tests to write, does this mean that your algorithm is perfect? Not necessary, unless there is a predefined set of rules. TDD does not guarantee bug-less code; it merely helps you write better code that can be better understood and modified.
Even better, if you do discover a bug, it’s that much easier to write a test that reproduces the bug. This way, you can ensure that the bug never occurs again – because you’ve tested for it!
Conclusion
You may argue that this process is not technically “TDD.” And you’re right! This example is closer to how many everyday programmers work.
Test-Driven Development is a process that can be both fun to practice, and hugely beneficial to the quality of your production code. Its flexibility in its application to large projects with many team members, right down to a small solo project means that it’s a fantastic methodology to advocate to your team.
Whether pair programming or developing by yourself, the process of making a failing test pass is hugely satisfying. If you’ve ever argued that tests weren’t necessary, hopefully this article has swayed your approach for future projects.
Thanks for reading!
-
Tyrel
-
ajkumar25
-
Kenneth Kinyanjui
-
Miao Zhen
-
-
http://www.the5fire.com/ the5fire
-
ajkumar25
-
http://www.the5fire.com/ the5fire
-
-
-
Nelson Rodrigues
-
ajkumar25
-
-
Howie
-
ajkumar25
-
Howie
-
ajkumar25
-
Howie
-
ajkumar25
-
-
-
-
-
Noah Yetter
-
ajkumar25
-
Noah Yetter
-
Nelson Rodrigues
-
ajkumar25
-
-
-
-
-
Nelson Rodrigues
-
ajkumar25
-
Nelson Rodrigues
-
ajkumar25
-
-
-

