This is the sixth post in the Django Blog Series. In this post we will be discussing how implementing Django unit testing can assist our app Charcha, discussion forum application in Django to achieve a better quality safety net. You can find the full code for the Charcha forum here.

Before Diving into writing the unit tests for our app, we first need to understand why do we actually need to write them and what is the apt scenarios that should be taken care of with tests.

Why Should You Test Your Code?

“Quality is never an accident; it is always the result of intelligent effort.” – John Ruskin

Providing automated tests for your code is a way to repeatedly ensure, with minimal developer effort, that the code you wrote to handle a task works as advertised. I like to think of tests as my insurance policy. They generally keep me from breaking existing code & looking foolish to other people. They’re also concrete proof that the code works correctly. Without that proof, what you have is a pile of code that worked right once on your machine & that you’ll either have to hand-test again & again in the future or will break without you knowing any wiser.

This is not to say that tests solve everything. There will always be bugs in software. Maybe the tests miss a code path or a user will use something in an unexpected way. But tests give you better confidence & a safety net.

Types Of Testing

There are many different types of testing. The prominent ones this series will cover are Django unit tests and integration tests.

Unit tests cover very small, highly specific areas of code. There are usually relatively few interactions with other areas of the software. This style of testing is very useful for critical, complicated components, such as validation, importing or methods with complex business logic.

Integration tests are at the opposite end of the spectrum. These tests usually cover multiple different facets of the application working together to produce a result. They ensure that data flow is right & often handle multiple user interactions.

The main difference between these two types is not the tooling but the approach and what you choose to test. It’s also a very common thing to mix & match these two types throughout your test suite as it is appropriate. In this post, we’ll focus only on unit testing.

When should you really test?

Another point of decision is deciding whether to do test-first (a.k.a. Test Driven Development) or test-after. Test-first is where you write the necessary tests to demonstrate the proper behavior of the code Before you write the code to solve the problem at hand. Test-after is when you’ve already written the code to solve the problem, then you go back & create tests to make sure the behavior of the code you wrote is correct.

This choice comes down to personal preference. An advantage of test-driven development is that it forces you to not skimp on the tests & think about the API up front. However, it feels very unnatural at first & if you have no experience writing tests, you may be at a loss as to what to do. Test-after feels more natural but can lead to weak tests if they’re hurried & not given the proper time/effort.

Something that is always appropriate, regardless of general style, is when you get a bug report. ALWAYS create a test case first & run your tests. Make sure it demonstrates the failure, THEN go fix the bug. If your fix is correct, that new test should pass! It’s an excellent way to sanity check yourself & is a great way to get started with testing to boot.

Let’s get to it

Now that we’ve got a solid foundation on the why what & when of testing, we’re going to start diving into code. When you run python manage.py startup, this automatically creates tests.py file within your app where we can write our tests.

Before writing the test cases we need to import TestCase module and models from our models.py. Here’s the snippet for it:

from django.test import TestCase from django.contrib.auth.models import AnonymousUser from .models import Post, Vote, Comment, User

Adding Tests to Views

class DiscussionTests(TestCase): def setUp(self): self._create_users() def _create_users(self): self.ramesh = User.objects.create_user( username="ramesh", password="top_secret") self.amit = User.objects.create_user( username="amit", password="top_secret") self.swetha = User.objects.create_user( username="swetha", password="top_secret") self.anamika = AnonymousUser() def new_discussion(self, user, title): post = Post(title=title, text="Does not matter", author=user) post.save() return post

This code sets up a new test case(DiscussionTests), which you can think of as a collection of related tests. Any method written here will be run automatically & its output will be included in the testing output when we run the command.

The setUp is run before and after every test respectively. This allows you to set up a basic context or environment inside of each of your tests. This also ensures that each of your tests does not edit the data that other tests depend on. This is a basic tenet of testing, that each test should stand alone, and not affect the others. This code snippet when ran creates the respective users for User objects and a Post object. We are going to use these as the base for meaningful tests that we will write in a bit.

If at this stage we run the command: python manage.py test discussions we’ll get the result something like this:

Creating test database for alias 'default'... ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK Destroying test database for alias 'default'...

Now, this is self-explanatory that no tests ran since we didn’t have any assertions in our tests right now. Before writing some of our tests we should also know about the test database that’s been created when we run our tests.

Tests that require a database (namely, model tests) will not use your “real” (production) database. Separate, blank databases are created for the tests.

Regardless of whether the tests pass or fail, the test databases are destroyed when all the tests have been executed.

You can prevent the test databases from being destroyed by using the test –keepdb option. This will preserve the test database between runs. If the database does not exist, it will first be created. Any migrations will also be applied in order to keep it up to date.

def test_double_voting(self): post = self.new_discussion(self.ramesh, "Ramesh's Biography") self.assertEquals(post.upvotes, 0) post.upvote(self.amit) post = Post.objects.get(pk=post.id) self.assertEquals(post.upvotes, 1) post.upvote(self.amit) post = Post.objects.get(pk=post.id) self.assertEquals(post.upvotes, 1) def test_comments_ordering(self): _c1 = "See my Biography!" _c2 = "Dude, this is terrible!" _c3 = "Why write your biography when you haven't achieved a thing!" _c4 = "Seriously, that's all you have to say?" post = self.new_discussion(self.ramesh, "Ramesh's Biography") self.assertEquals(post.num_comments, 0) rameshs_comment = post.add_comment(_c1, self.ramesh) amits_comment = rameshs_comment.reply(_c2, self.amit) swethas_comment = rameshs_comment.reply(_c3, self.swetha) rameshs_response = amits_comment.reply(_c4, self.ramesh) comments = [c.text for c in Comment.objects.best_ones_first(post.id, self.ramesh.id)] self.assertEquals(comments, [_c1, _c2, _c4, _c3]) # check if num_comments in post object is updated post = Post.objects.get(pk=post.id) self.assertEquals(post.num_comments, 4)

As we discussed above, we need to determine the apt scenarios which should be handled with test cases in order to maintain quality assurance. So the above snippet demonstrates a couple of scenarios for which we wrote test cases for our discussion forum. The tests are enough self-explanatory, wherein the first one we check if there is no double voting done by a particular user. The test consists of first starting of a new discussion and then a series of upvotes and assertions checking if the result is as we expected it to be. Whereas, in the second example we are assuring the correct order and count of the comments that are being added to a discussion.

On running tests now, the result is:

Creating test database for alias 'default'... .. ---------------------------------------------------------------------- Ran 2 tests in 0.532s OK Destroying test database for alias 'default'...

Both of our tests passed and the test database created was destroyed.

Summary

Unit tests lay a solid foundation on which the rest of your testing process should be built. Opening your app up and taking the new feature you just developed for a test drive is always a good practice. So I think we can come down to mainly 3 reasons why unit testing our app can be a boon for us:

Knowing if our code really works. It saves us a lot of pain (and time). It makes deployments a snap.

Happy unit testing folks!