In the previous 3 parts, we’ve looked at creating a simple polling app which involved creating views and templates. We also made it more robust by generalizing the views where possible. In this part, we will look at how to test our application.
What Are Automated Tests?
Tests are routines that check how your code is working. This could be done on lots of levels. Perhaps you want to check if a specific function returns the values you expect with given inputs or maybe you want to check to make sure a sequence of inputs on your website directs the user to the desired result.
An automated test will do some of the testing for you. You create the tests once then when we make changes to our app, we can check the functionality of it by running the tests we already designed. This is good for a lot of reasons:
It saves time
This one is obvious, right? If we aren’t manually doing the same tests over and over, we’ll save ourselves a lot of time. Also, if your project ends up with multiple apps, we might want to do many, many tests. If these are automated, we’ll also be saving ourselves a lot of time.
Testing Can Prevent Problems
Even with your own code, you might not fully understand how everything works. When we get into the habit of continually testing, we learn more about the code and in the end we learn more about it. Your tests very well may show you issues that you didn’t know you had (or would have) simply because you don’t quite understand the inner workings of the code (which is fine!).
Other People Like To See Your Tests
Some people feel like “code without tests is broken by design”. They want to see that you are testing your code and how you are testing your code.
It Will Help Your Team Collaborate
Tests will ensure that other people on your team won’t accidentally break your code. This goes along with the second point that tests will check for things even if you don’t fully understand how the things are working.
There are a few ways that people approach programmatically testing their software. Some people follow test-driven development, which means they write their tests before they write their application. Other times people will write some code and then write some tests. Sometimes, it’s hard to even know where to get started and what you should test.
Identify a Bug
We actually already have a bug in our code. Within the polls application, the Question.was_published_recently() method returns correctly True if the question was published within the last day but also if the pub_date field is in the future (which is incorrect).
Lets check to make sure this is the case. Open the interactive shell (to do this, navigate to the manage.py file and type the command “python manage.py shell”). We will programmatically add a new question in the future by typing the following into the shell:
import datetime from django.utils import timezone from polls.models import Question future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
This creates a question that is 30 days into the future. Now, lets run the was_published_recently() method to see what it returns. We want it to return False (because 30 days into the future is not recently). To test it, run:
This will return, incorrectly, True.
Create a Test to Expose the Bug
Conventionally, we put our tests in a file called tests.py. This will allow the testing system to automatically find the tests. Within your polls directory you should find a file with this name. Add the following to it:
import datetime from django.utils import timezone from django.test import TestCase from .models import Question class QuestionMethodTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently() should return False for questions whose pub_date is in the future. """ time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertEqual(future_question.was_published_recently(), False)
This creates a django.test.TestCase sublcass with a method that does what we manually tested previously. We create an instance of Question that has a pub_date in the future and compares it with False (which is what the correct answer should be).
Run the Test
Navigate to the directory with the manage.py file and run the following command in the command prompt:
python manage.py test polls
Your output should be something like this:
Creating test database for alias 'default'... F ====================================================================== FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "E:\Dropbox\Dropbox\django\pantrio\polls\tests.py", line 21, in test_was_published_recently_with_future_question self.assertEqual(future_question.was_published_recently(), False) AssertionError: True != False ---------------------------------------------------------------------- Ran 1 test in 0.002s FAILED (failures=1) Destroying test database for alias 'default'...
Running this command did several things. First, it looked within the polls application for your test file (tests.py). It found the subclass for the test case (django.test.TestCase). It isn’t obvious, but it created a database for our testing (which is impressive and really interesting to me, now that I understand more about databases). It then looked at the methods and paid attention to the ones that begin with test (for us, this is test_was_published_recently_with_future_question). It then ran the code within that function (so, it created the Question instance with a publication date 30 days in the future). Finally, it compared the output of the was_published_recently method to what we think it should be (False) by using the assertEqual() method.
Since the actual output was True, it returns ‘FAILED’ to the command prompt.
Fix the Bug
Testing the bug is only half of the battle. After we find and understand the bugs, we have to fix them. For this bug, we want to return False if the date is in the future. To do this, open the models.py file in the polls directory.
The current was_published_recently() method looks like this:
def was_published_recently(self): return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
We want to check what today’s date is and return True only if the publication date is between yesterday and today. To do this, amend the function to look like this:
def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now
Rerun the test to see if this worked:
Creating test database for alias 'default'... . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'...
To recap, we first identified a bug. Next, we wrote a test to expose it. Finally, we corrected the bug and confirmed that it was fixed by running the test again. A great feature about running tests is that no matter what we change in the future, we can be sure that we won’t get this bug unknowingly again because every time we run our tests, we will be checking for it.
More Comprehensive Tests
Lets make sure that was_published_recently works for past, recent, and future questions. Add the following to the tests.py file:
def test_was_published_recently_with_old_question(self): """ was_published_recently() should return False for questions whose pub_date is older than 1 day. """ time = timezone.now() - datetime.timedelta(days=30) old_question = Question(pub_date=time) self.assertEqual(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently() should return True for questions whose pub_date is within the last day. """ time = timezone.now() - datetime.timedelta(hours=1) recent_question = Question(pub_date=time) self.assertEqual(recent_question.was_published_recently(), True)
That’s it for today. Next time, we’ll look into how to test views and customizing the look and feel of our app.
Have questions or suggestions? Please feel free to comment below or contact me.