Test Driven Development With Python & Django using PyCharm
Mar 22, 2016 11:44 · 1132 words · 6 minute read
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
Create test cases
Based on requirements let’s draft some test cases (commit @a8402e0) and empty test functions for a Products API @aa49d2c
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
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).
Of course tests passed which is bad because of the reasons explained above.
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.
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 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
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