tdd_fe

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:

  1. You are not allowed to write any production code, if there is not a failing test to warrant it.
  2. You are not allowed to write more of a unit test than is strictly necessary to make it fail. Not compiling / running is failing.
  3. 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

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)

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.

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:

There should be a main folder for source classes, and a Tests/ file, naturally, for the tests.

Lets write our first test

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

When you run the test above, you will receive a Import Error message, like the following:

Yikes! We should do something about it. Create an empty Article model in the project’s models.py.

That’s it. If you run the test again, it passes. Congratulations on your first test!

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.

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

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.

Terminal will output something like this

Now run migrate command to migrate the model schema.

Now again rerun your test and you again get an failing test

Add the title attribute to your models and again run migrations commands to ship the title attrribute to the Article table in the database.

Now run the migration command.

This time we have to pass --auto as the command line arguement which will pick up the changes in model automatically.

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

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. )
tdd2

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.

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.

To make it pass, add 'django.contrib.admin', in your settings.py and define the admin urls in project’s url.py file.

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:

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

Now lets write the tests for it in the same tests.py.

 

  • 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

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:

Now add this fixture in your ArticleAdminTest.

You can get the failing test again with this AssertionError

Create admin.py inside your app folder and register our small tested model.

Now run the tests again and you will see that our tests pass.

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.

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.

if you run the tests, it will fail, so lets add those fields in models and then run the test.

Our Tests pass now, so lets add some code and again check if something is broken or not.

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.

Now run the tests and watch our last test fail.

Lets fix that, open up your views.py and write this code

Now we get another AssertionError stating that URL is not found on ‘/’

So, our assumption now is we have to define the url’s for index.
Open urls.py and add this line

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

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

    What IDE do you use that has a test bar like that?

    • ajkumar25

      I use Pycharm. Its awesome.

    • Kenneth Kinyanjui

      Tyrel, you can get the Pycharm Community edition for starters

    • Miao Zhen

      Wing IDE is much lighter and great.

  • http://www.the5fire.com/ the5fire

    Great article! But how to deal with the TEST data?

    • ajkumar25

      I dont think i understood you properly. Can you please elaborate your question ?

  • Nelson Rodrigues

    I’m a newbie to python and had some trouble in getting migrations to work but found a solution in this StackOverflow answer: http://stackoverflow.com/a/5687690/433873

    Hopefully it will be of some use to someone else.

    • ajkumar25

      Thanks for this !

  • Howie

    Easy to follow article which explains TDD concepts well. However, the examples aren’t strictly unit tests – they rely on the database and later on a webserver, so really they are integration tests.

    I’d also argue that your examples aren’t really testing your code, but actually Django’s core code. Do you need test that the model’s save() method works? That has been covered by Django’s own suite of tests.

    • ajkumar25

      Without employing save() how will you test your models ?

      • Howie

        Well, unless the save() method has been extended, why test it? I only test methods on the model that I have added or extended, without hitting the database

        • ajkumar25

          save() doesn’t hit the production database at all.

          The test database gets created and tests are carried out on that test database and once tests complete, they are destroyed.

          • Howie

            Oh. I’m fully aware about the test db etc. My point is that the test serves no purpose. It’s a repeat of the django framework’s own set of tests. I trust their tests to ensure that the save() method works. All I care about is testing the methods I have explicitly written. Plus, not hitting the db will speed up the tests

          • ajkumar25

            Ah, i misunderstood you.

            The article is more about demonstrating how stuff works. If i would have written a rather complex custom template and view. What would have been the point? You just wanna know how to test the frontend. And the admin and the basic models is well suited for that case.

  • Noah Yetter

    Not very convincing to this TDD skeptic. Every example from the very first line assumes a comprehensive knowledge of Django as a starting point. How would one ever begin? Oh right, you have to write some actual code that does things to figure out what you’re doing. Writing tests first is not conducive to experimentation or learning.

    • ajkumar25

      Well, writing tests first and then code are the pillars of TDD.

      You need to think of the possible structure of models or something else you are going to build then write tests for that structure and then continue with productive code. Thats what the whole world of TDD is about.

      You cannot write production code at first.

      • Noah Yetter

        Since people have been writing code since the 50′s and TDD wasn’t a thing until the 2000′s, yes, you can “write production code at first”.

        I’m arguing that frequently you CAN’T envision what you want to build well enough to write the tests first. Does anyone have an argument to answer this challenge, or just more TDD dogma?

        • Nelson Rodrigues

          > I’m arguing that frequently you CAN’T envision what you want to build well enough to write the tests first

          I think this is the challenge of TDD, you need to know what are all the components needed for the application before doing anything.

          • ajkumar25

            @nyetter:disqus Without making assumptions what you will be needing for your system, how could you even start the project ?

            Writing tests first gives assurance that what we have coded is correct or approximately correct and we can easily refactor.

  • Nelson Rodrigues

    I found a error: When creating the model, you name it “Article” first but in your code is in plural form. This brings a error later on when testing for the existance of “articles” string in the administration panel, because Django adds an extra “s” to the models and now it reads “Articless”.

    The test for “articles” string is also case sensitive.

    • ajkumar25

      Really you had this error or just assuming that it will happen ?

      Actually the “find_elements_by_link_text” checks the **links** in the html body, even though django admin shows it as “articless”, the links are still “articles” only so our tests passes.

      Hope its clear.

      Cheers :)

      • Nelson Rodrigues

        I experienced it.

        This looks up the text in the link not inside the url and fails:
        self.browser.find_elements_by_link_text(‘articles’)

        • ajkumar25

          What error are you getting ? Can you show the traceback ?

          I ran the tests and all tests passed for me.

  • Laurent

    I just discovered this website and I’m hooked! I went through this whole tutorial. Not everything went smoothly but I was able to follow all the steps (except for the last one, I still get a template not found error). But overall great content!