Test Driven Development With Python & Django using PyCharm

Mar 22, 2016 11:44 · 1132 words · 6 minute read Python Django guide

The purpose of this quick guide is to show how simple is it to start doing test driven development with Python and Django using PyCharm. Even advanced programmers often scare to do TDD because they simple don’t know where to start. In fact, it is extremely simple and natural way of development. Source code of this example is available on the git repository.

Requirement Specification

It is highly important to have clear requirements. Please, read another article on how to do requirements spec. Let’s assume that we have an online store with a product catalogue. This is a mobile application that allows users to buy things and we are building an API for that. User stories we will be working on:

As a User, I want to see a list of Products.
As a User, I want to get Product details.
As a User, I want to purchase a Product.
As a User, I want to leave a Review.
As a User, I want to know my Discount.

Of course these are very simplified requirements. It is very important to define the requirements carefully but it will be just fine four our example. So let’s start coding!

TDD using PyCharm

Below is a step by step guide on how to write tests ahead. It is very simple and fits best for those who never did that before.

Let’s create a Django project

Simply, execute

python manage.py startproject test_project

View commit @8f20953

Empty Django project

Create test cases

Based on requirements let’s draft some test cases (commit @a8402e0) and empty test functions for a Products API @aa49d2c

Drafted test cases

UPDATE: this is actually not quite right. The way I bootstrap’ed tests is not correct for several reasons. Let’s take a closer look:

class ProductApiTestCase(TestCase):
    """
    Test case covers API for getting details of the products such as:
    GET /api/products/
    GET /api/products/pk/
    """

    def test_get_product_list(self):
        pass

    def test_product_details(self):
        pass
    
    def test_get_non_existent_product_details(self):
        pass
        
This code will actually produce “green” results and tests will pass. Which means technically incorrect behaviour since tests were never implemented. The consequence is confusion. In case there are dozens of such blank tests which just pass, it is easy to forget to implement some of them. Instead, it should be like that:

class ProductApiTestCase(TestCase):
    """
    Test case covers API for getting details of the products such as:
    GET /api/products/
    GET /api/products/pk/
    """
    NOT_IMPLEMENTED = "The test has not been implemented"

    def test_get_product_list(self):
        self.fail(self.NOT_IMPLEMENTED)

    def test_product_details(self):
        self.fail(self.NOT_IMPLEMENTED)

    def test_get_non_existent_product_details(self):
        self.fail(self.NOT_IMPLEMENTED)

This will guarantee failure unless test is implemented. Another possibility though is to skip the test like this

NOT_IMPLEMENTED = "The test has not been implemented"

@skip(NOT_IMPLEMENTED)
def test_get_product_list(self):
    pass

You could even write your own decorator

def skipNotImplemented():
    """
    Skip a test because it is not implemented.
    """
    NOT_IMPLEMENTED = "The test has not been implemented"
    return skip(NOT_IMPLEMENTED)

Personally, I like the idea of skipping test more than failing because there is no reason to even start a test while it is not implemented. Technically, it’s not that something is not working but rather should not be tested yet because the test is not defined.

Time to run tests. Select Run -> Run in PyCharm or hit + + R on Mac (ctrl + alt + R). It will ask you what do you want to do. Simply select Test: your_app.tests.YourTestCase with Django icon. Afterwards you can repeat the last run with just + R (ctrl + R).

Run for the first time

Of course tests passed which is bad because of the reasons explained above.

Empty test results

Make tests fail

Now, let’s make them fail! I am going to create three test cases. Each of them will send a GET request to the API and compare the response to the expected results.

def test_product_details(self):
    expected_results = {
        'id': 1,
        'title': 'My Product',
        'description': 'My Description',
        'price': 100
    }
    response = self.client.get('/api/products/1/')
    actual_results = response.json()

    self.assertEqual(expected_results, actual_results)

It is a good idea to explicitly define expected_results so that anyone can use the test method as a documentation. Not, that the URL is hard-coded. Usage of method reverse has little benefits and do more harm than good:

Pros of using reverse in tests

  • No need to re-write the test whn URL is changed
  • Defers actual URL decisions to an implementation phase

Cons of using reverse in tests

  • Defining a name of the URL is also a hard-code and must be re-written in case of a change
  • Still need to make decisions regarding URL naming
  • Implicit URL definition may confuse
  • The url remains unknown unless you search for a URL route definition which makes reading tests harder

The implementation can be found here @d0a305c.

Product test cases

Note, that tests go first. I don’t know exact implementation of my methods yet but I can assume the flow. Structure your tests to make it small and run fast. Clearly state the output you expect. Put a single action and preferably a single assertion statement. Keep in mind that tests could be used as a reference, documentation and example of usage so make it clean and easy to read.

2 failed, 1 passed

2 out of 3 of the tests failed. However, there is a single test that succeeded without me doing anything! This is due the fact that Django has HTTP 404 handler by default. This made the following test pass:

def test_get_non_existent_product_details(self):
    response = self.client.get('/api/products/1/')
    self.assertEqual(response.status_code, 404)

Implementation of the rest of the tests

Add a model with migrations @b2e089737. Add views and serializers @ddebe67. You can run a single test for development purposes as shown below. Put cursor to a test you want to run and select Run -> Run just like before. PyCharm will give you an option to run an individual test.

Good, we have made the 1st test to pass

1st test passed

Further coding. Next day

Here is where I called it a day. I wanted to finish ProductDetailApiTest case but something wasn’t working. It was late in the evening so I decided to continue on the next morning. The huge plus of a test-driven approach is that it is very easy to pick up where you left off with your code, after a break. There is no need to recall the context I worked on. When I came back the next day, the first thing I did was to run tests. The only test left was ProductDetailAPI so I started my work in this direction.

My problem was urls.py where I made a typo. The problem was solved at @b5a02bd0ed

Conclusion

Test Driven Development using PyCharm is very simple. Just try it. PyCharm made TDD even easier than it was before.

  • Write tests first. Write them in the morning
  • Write only tests for the features requested. This is a separate topic of how important to avoid unnecessary work
  • Keep tests small
  • Keep them easy to read
  • Make them fast (run within a seconds)
  • Run them often