| | 1 | ===================================== |
| | 2 | Writing your first Django app, part 5 |
| | 3 | ===================================== |
| | 4 | |
| | 5 | This tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left off. |
| | 6 | We've built a Web-poll application, and we'll now create some automated tests |
| | 7 | for it. |
| | 8 | |
| | 9 | Introducing automated testing |
| | 10 | ============================= |
| | 11 | |
| | 12 | What are automated tests? |
| | 13 | ------------------------- |
| | 14 | |
| | 15 | Tests are simple routines that check the operation of your code. |
| | 16 | |
| | 17 | Testing operates at different levels. Some tests might apply to a tiny detail |
| | 18 | - *does a particular model method return values as expected?*, while others |
| | 19 | examine the overall operation of the software - *does a sequence of user inputs |
| | 20 | on the site produce the desired result?* That's no different from the kind of |
| | 21 | testing you did earlier in :doc:`Tutorial 1 </intro/tutorial01>`, using the |
| | 22 | shell to examine the behavior of a method, or running the application and |
| | 23 | entering data to check how it behaves. |
| | 24 | |
| | 25 | What's different in *automated* tests is that the testing work is done for |
| | 26 | you by the system. You create a set of tests once, and then as you make changes |
| | 27 | to your app, you can check that your code still works as you originally |
| | 28 | intended, without having to perform time consuming manual testing. |
| | 29 | |
| | 30 | Why you need to create tests |
| | 31 | ---------------------------- |
| | 32 | |
| | 33 | So why create tests, and why now? |
| | 34 | |
| | 35 | You may feel that you have quite enough on your plate just learning |
| | 36 | Python/Django, and having yet another thing to learn and do may seem |
| | 37 | overwhelming and perhaps unnecessary. After all, our polls application is |
| | 38 | working quite happily now; going through the trouble of creating automated |
| | 39 | tests is not going to make it work any better. If creating the polls |
| | 40 | application is the last bit of Django programming you will ever do, then true, |
| | 41 | you don't need to know how to create automated tests. But, if that's not the |
| | 42 | case, now is an excellent time to learn. |
| | 43 | |
| | 44 | Tests will save you time |
| | 45 | ~~~~~~~~~~~~~~~~~~~~~~~~ |
| | 46 | |
| | 47 | Up to a certain point, 'checking that it seems to work' will be a satisfactory |
| | 48 | test. In a more sophisticated application, you might have dozens of complex |
| | 49 | interactions between components. |
| | 50 | |
| | 51 | A change in any of those components could have unexpected consequences on the |
| | 52 | application's behavior. Checking that it still 'seems to work' could mean |
| | 53 | running through your code's functionality with twenty different variations of |
| | 54 | your test data just to make sure you haven't broken something - not a good use |
| | 55 | of your time. |
| | 56 | |
| | 57 | That's especially true when automated tests could do this for you in seconds. |
| | 58 | If something's gone wrong, tests will also assist in identifying the code |
| | 59 | that's causing the unexpected behavior. |
| | 60 | |
| | 61 | Sometimes it may seem a chore to tear yourself away from your productive, |
| | 62 | creative programming work to face the unglamorous and unexciting business |
| | 63 | of writing tests, particularly when you know your code is working properly. |
| | 64 | |
| | 65 | However, the task of writing tests is a lot more fulfilling than spending hours |
| | 66 | testing your application manually or trying to identify the cause of a |
| | 67 | newly-introduced problem. |
| | 68 | |
| | 69 | Tests don't just identify problems, they prevent them |
| | 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| | 71 | |
| | 72 | It's a mistake to think of tests merely as a negative aspect of development. |
| | 73 | |
| | 74 | Without tests, the purpose or intended behavior of an application might be |
| | 75 | rather opaque. Even when it's your own code, you will sometimes find yourself |
| | 76 | poking around in it trying to find out what exactly it's doing. |
| | 77 | |
| | 78 | Tests change that; they light up your code from the inside, and when something |
| | 79 | goes wrong, they focus light on the part that has gone wrong - *even if you |
| | 80 | hadn't even realized it had gone wrong*. |
| | 81 | |
| | 82 | Tests make your code more attractive |
| | 83 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| | 84 | |
| | 85 | You might have created a brilliant piece of software, but you will find that |
| | 86 | many other developers will simply refuse to look at it because it lacks tests; |
| | 87 | without tests, they won't trust it. Jacob Kaplan-Moss, one of Django's |
| | 88 | original developers, says "Code without tests is broken by design." |
| | 89 | |
| | 90 | That other developers want to see tests in your software before they take it |
| | 91 | seriously is yet another reason for you to start writing tests. |
| | 92 | |
| | 93 | Basic testing strategies |
| | 94 | ======================== |
| | 95 | |
| | 96 | There are many ways to approach writing tests. |
| | 97 | |
| | 98 | Some programmers follow a discipline called "`test-driven development`_"; they |
| | 99 | actually write their tests before they write their code. This might seem |
| | 100 | counter-intuitive, but in fact it's similar to what most people will often do |
| | 101 | anyway: they describe a problem, then create some code to solve it. Test-driven |
| | 102 | development simply formalizes the problem in a Python test case. |
| | 103 | |
| | 104 | More often, a newcomer to testing will create some code and later decide that |
| | 105 | it should have some tests. Perhaps it would have been better to write some |
| | 106 | tests earlier, but it's never too late to get started. |
| | 107 | |
| | 108 | Sometimes it's difficult to figure out where to get started with writing tests. |
| | 109 | If you have written several thousand lines of Python, choosing something to |
| | 110 | test might not be easy. In such a case, it's fruitful to write your first test |
| | 111 | the next time you make a change, either when you add a new feature or fix a bug. |
| | 112 | |
| | 113 | So let's do that right away. |
| | 114 | |
| | 115 | .. _test-driven development: http://en.wikipedia.org/wiki/Test-driven_development/ |
| | 116 | |
| | 117 | Writing our first test |
| | 118 | ====================== |
| | 119 | |
| | 120 | We identify a bug |
| | 121 | ----------------- |
| | 122 | |
| | 123 | Fortunately, there's a little bug in the ``polls`` application for us to fix |
| | 124 | right away: the ``Poll.was_published_recently()`` method returns ``True`` if |
| | 125 | the ``Poll`` was published within the last day (which is correct) but also if |
| | 126 | the ``Poll``'s ``pub_date`` field is in the future (which certainly isn't). |
| | 127 | |
| | 128 | You can see this in the Admin; create a Poll whose date lies in the future; |
| | 129 | you'll see that the ``Poll`` change list claims it was published recently. |
| | 130 | |
| | 131 | You can also see this using the shell:: |
| | 132 | |
| | 133 | >>> import datetime |
| | 134 | >>> from django.utils import timezone |
| | 135 | >>> from polls.models import Poll |
| | 136 | >>> # create a Poll instance with pub_date 30 days in the future |
| | 137 | >>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30)) |
| | 138 | >>> # was it published recently? |
| | 139 | >>> future_poll.was_published_recently() |
| | 140 | True |
| | 141 | |
| | 142 | Since things in the future are not 'recent', this is clearly wrong. |
| | 143 | |
| | 144 | Create a test to expose the bug |
| | 145 | ------------------------------- |
| | 146 | |
| | 147 | What we've just done in the shell to test for the problem is exactly what we |
| | 148 | can do in an automated test, so let's turn that into an automated test. |
| | 149 | |
| | 150 | The best place for an application's tests is in the application's ``tests.py`` |
| | 151 | file - the testing system will look there for tests automatically. |
| | 152 | |
| | 153 | Put the following in the ``tests.py`` file in the ``polls`` application (you'll |
| | 154 | notice ``tests.py`` contains some dummy tests, you can remove those):: |
| | 155 | |
| | 156 | import datetime |
| | 157 | |
| | 158 | from django.utils import timezone |
| | 159 | from django.test import TestCase |
| | 160 | |
| | 161 | from polls.models import Poll |
| | 162 | |
| | 163 | # we'll put all the Poll method tests in a class together |
| | 164 | class PollMethodTests(TestCase): |
| | 165 | |
| | 166 | def test_was_published_recently_with_future_poll(self): |
| | 167 | # create a Poll instance whose pub_date is in the future |
| | 168 | future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30)) |
| | 169 | # was_published_recently() should return False |
| | 170 | self.assertEqual(future_poll.was_published_recently(), False) |
| | 171 | |
| | 172 | What we have done here is created a :class:`django.test.TestCase` subclass |
| | 173 | with a method that creates a ``Poll`` instance with a ``pub_date`` in the |
| | 174 | future. We then check the output of ``was_published_recently()`` - which |
| | 175 | *ought* to be False. |
| | 176 | |
| | 177 | Running tests |
| | 178 | ------------- |
| | 179 | |
| | 180 | In the terminal, we can run our test:: |
| | 181 | |
| | 182 | python manage.py test polls |
| | 183 | |
| | 184 | and you'll see something like:: |
| | 185 | |
| | 186 | Creating test database for alias 'default'... |
| | 187 | F |
| | 188 | ====================================================================== |
| | 189 | FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests) |
| | 190 | ---------------------------------------------------------------------- |
| | 191 | Traceback (most recent call last): |
| | 192 | File "/home/daniele/django-testing-tutorial/mysite/polls/tests.py", line 18, in test_was_published_recently_with_future_poll |
| | 193 | self.assertEqual(future_poll.was_published_recently(), False) |
| | 194 | AssertionError: True != False |
| | 195 | |
| | 196 | ---------------------------------------------------------------------- |
| | 197 | Ran 1 test in 0.000s |
| | 198 | |
| | 199 | FAILED (failures=1) |
| | 200 | Destroying test database for alias 'default'... |
| | 201 | |
| | 202 | What happened is this: |
| | 203 | |
| | 204 | * ``python manage.py test polls`` looked for tests in the ``polls`` application |
| | 205 | |
| | 206 | * it found a subclass of the :class:`django.test.TestCase` class |
| | 207 | |
| | 208 | * it created a special database for the purpose of testing |
| | 209 | |
| | 210 | * it looked for test methods - ones whose names begin with ``test`` |
| | 211 | |
| | 212 | * in ``test_was_published_recently_with_future_poll`` it created a ``Poll`` |
| | 213 | instance whose ``pub_date`` field is 30 days in the future |
| | 214 | |
| | 215 | * ... and using the ``assertEqual()`` method, it discovered that its |
| | 216 | ``was_published_recently()`` returns ``True``, though we wanted it to return |
| | 217 | `False`` |
| | 218 | |
| | 219 | The test informs us which test failed - |
| | 220 | ``test_was_published_recently_with_future_poll`` - and even the line on which |
| | 221 | the failure occurred. |
| | 222 | |
| | 223 | Fixing the bug |
| | 224 | -------------- |
| | 225 | |
| | 226 | We already know what the problem is: ``Poll.was_published_recently()`` should |
| | 227 | return ``False`` if its ``pub_date`` is in the future. Amend the method in |
| | 228 | ``models.py``, so that it will only return ``True`` if the date is also in the |
| | 229 | past:: |
| | 230 | |
| | 231 | def was_published_recently(self): |
| | 232 | return self.pub_date >= timezone.now() - datetime.timedelta(days=1) and \ |
| | 233 | self.pub_date < timezone.now() |
| | 234 | |
| | 235 | and run the test again:: |
| | 236 | |
| | 237 | Creating test database for alias 'default'... |
| | 238 | . |
| | 239 | ---------------------------------------------------------------------- |
| | 240 | Ran 1 test in 0.000s |
| | 241 | |
| | 242 | OK |
| | 243 | Destroying test database for alias 'default'... |
| | 244 | |
| | 245 | Having identified a bug, we have written a test that exposes it, and corrected |
| | 246 | the bug in the code. |
| | 247 | |
| | 248 | Many other things might go wrong with our application in the future, but we can |
| | 249 | be sure that we won't inadvertently reintroduce this bug, because simply |
| | 250 | running the test will warn us immediately. We can consider this little portion |
| | 251 | of the application pinned down safely forever. |
| | 252 | |
| | 253 | More comprehensive tests |
| | 254 | ------------------------ |
| | 255 | |
| | 256 | While we're here, we can further pin down the ``was_published_recently()`` |
| | 257 | method; in fact, it would be positively embarrassing if in fixing one bug we had |
| | 258 | introduced another. |
| | 259 | |
| | 260 | Add two more test methods to the same class, to test the behavior of the method |
| | 261 | more comprehensively:: |
| | 262 | |
| | 263 | def test_was_published_recently_with_old_poll(self): |
| | 264 | # create a Poll instance whose pub_date is 30 days ago |
| | 265 | old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30)) |
| | 266 | # was_published_recently() should return False |
| | 267 | self.assertEqual(old_poll.was_published_recently(), False) |
| | 268 | |
| | 269 | def test_was_published_recently_with_recent_poll(self): |
| | 270 | # create a Poll instance whose pub_date is one hour ago |
| | 271 | recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1)) |
| | 272 | # was_published_recently() should return True |
| | 273 | self.assertEqual(recent_poll.was_published_recently(), True) |
| | 274 | |
| | 275 | And now we have three tests, that confirm that ``Poll.was_published_recently()`` |
| | 276 | returns sensible values for past, recent, and future Polls. |
| | 277 | |
| | 278 | Again, ``polls`` is a simple application, but however complex it grows in the |
| | 279 | future and whatever other code it interacts with, we now have some guarantee |
| | 280 | that the method we have written tests for will behave in expected ways. |
| | 281 | |
| | 282 | Test a view |
| | 283 | =========== |
| | 284 | |
| | 285 | The polls application is fairly undiscriminating: it will publish any Poll, |
| | 286 | including ones whose ``pub_date`` field lies in the future. |
| | 287 | |
| | 288 | We should improve this. Setting a ``pub_date`` in the future should mean that |
| | 289 | the Poll is published at that moment, but invisible until then. |
| | 290 | |
| | 291 | A test for a view |
| | 292 | ----------------- |
| | 293 | |
| | 294 | When we fixed the bug above, we wrote the test first and then the code to fix |
| | 295 | it. In fact that was a simple example of test-driven development, but it |
| | 296 | doesn't really matter in which order we do the work. |
| | 297 | |
| | 298 | In our first test, we focused closely on the internal behavior of the code. For |
| | 299 | this test, we want to check its behavior as it would be experienced by a user |
| | 300 | through a web browser. |
| | 301 | |
| | 302 | Before we try to fix anything, let's have a look at the tools at our disposal. |
| | 303 | |
| | 304 | The Django test client |
| | 305 | ---------------------- |
| | 306 | |
| | 307 | Django provides a test :class:`~django.test.client.Client` to simulate a user |
| | 308 | interacting with the code at the view level. We can use it in ``tests.py`` |
| | 309 | or even in the shell. |
| | 310 | |
| | 311 | We will start again with the shell, where we need to do a couple of things that |
| | 312 | won't be necessary in ``tests.py``. The first is to set up the test environment |
| | 313 | in the shell:: |
| | 314 | |
| | 315 | >>> from django.test.utils import setup_test_environment |
| | 316 | >>> setup_test_environment() |
| | 317 | |
| | 318 | Next we need to import the test client class (later in ``tests.py`` we will use |
| | 319 | the :class:`django.test.TestCase` class, which comes with its own client, so |
| | 320 | this won't be required):: |
| | 321 | |
| | 322 | >>> from django.test.client import Client |
| | 323 | >>> # create an instance of the client for our use |
| | 324 | >>> client = Client() |
| | 325 | |
| | 326 | With that ready, we can ask the client to do some work for us:: |
| | 327 | |
| | 328 | >>> # get a response from '/' |
| | 329 | >>> response = client.get('/') |
| | 330 | >>> # we should expect a 404 from that address |
| | 331 | >>> response.status_code |
| | 332 | 404 |
| | 333 | >>> # on the other hand we should expect to find something at '/polls/' |
| | 334 | >>> # we'll use 'reverse()' rather than a harcoded URL |
| | 335 | >>> from django.core.urlresolvers import reverse |
| | 336 | >>> response = client.get(reverse('polls:index')) |
| | 337 | >>> response.status_code |
| | 338 | 200 |
| | 339 | >>> response.content |
| | 340 | '\n\n\n <p>No polls are available.</p>\n\n' |
| | 341 | >>> # note - you might get unexpected results if your ``TIME_ZONE`` |
| | 342 | >>> # in ``settings.py`` is not correct. If you need to change it, |
| | 343 | >>> # you will also need to restart your shell session |
| | 344 | >>> from polls.models import Poll |
| | 345 | >>> from django.utils import timezone |
| | 346 | >>> # create a Poll and save it |
| | 347 | >>> p = Poll(question="Who is your favourite Beatle?", pub_date=timezone.now()) |
| | 348 | >>> p.save() |
| | 349 | >>> # check the response once again |
| | 350 | >>> response = client.get('/polls/') |
| | 351 | >>> response.content |
| | 352 | '\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favourite Beatle?</a></li>\n \n </ul>\n\n' |
| | 353 | >>> response.context['latest_poll_list'] |
| | 354 | [<Poll: Who is your favourite Beatle?>] |
| | 355 | |
| | 356 | Improving our view |
| | 357 | ------------------ |
| | 358 | |
| | 359 | The list of polls shows polls that aren't published yet (i.e. those that have a |
| | 360 | ``pub_date`` in the future). Let's fix that. |
| | 361 | |
| | 362 | In :doc:`Tutorial 4 </intro/tutorial04>` we deleted the view functions from |
| | 363 | ``views.py`` in favor of a :class:`~django.views.generic.list.ListView` in |
| | 364 | ``urls.py``:: |
| | 365 | |
| | 366 | url(r'^$', |
| | 367 | ListView.as_view( |
| | 368 | queryset=Poll.objects.order_by('-pub_date')[:5], |
| | 369 | context_object_name='latest_poll_list', |
| | 370 | template_name='polls/index.html'), |
| | 371 | name='index'), |
| | 372 | |
| | 373 | ``response.context_data['latest_poll_list']`` extracts the data this view |
| | 374 | places into the context. |
| | 375 | |
| | 376 | We need to amend the line that gives us the ``queryset``:: |
| | 377 | |
| | 378 | queryset=Poll.objects.order_by('-pub_date')[:5], |
| | 379 | |
| | 380 | Let's change the queryset so that it also checks the date by comparing it with |
| | 381 | ``timezone.now()``. First we need to add an import:: |
| | 382 | |
| | 383 | from django.utils import timezone |
| | 384 | |
| | 385 | and then we must amend the existing url function:: |
| | 386 | |
| | 387 | url(r'^$', |
| | 388 | ListView.as_view( |
| | 389 | queryset=Poll.objects.order_by('-pub_date')[:5], |
| | 390 | context_object_name='latest_poll_list', |
| | 391 | template_name='polls/index.html'), |
| | 392 | name='index'), |
| | 393 | |
| | 394 | to:: |
| | 395 | |
| | 396 | url(r'^$', |
| | 397 | ListView.as_view( |
| | 398 | queryset=Poll.objects.filter(pub_date__lte=timezone.now()) \ |
| | 399 | .order_by('-pub_date')[:5], |
| | 400 | context_object_name='latest_poll_list', |
| | 401 | template_name='polls/index.html'), |
| | 402 | name='index'), |
| | 403 | |
| | 404 | ``Poll.objects.filter(pub_date__lte=timezone.now())`` returns a queryset |
| | 405 | containing Polls whose ``pub_date`` is less than or equal to - that is, earlier |
| | 406 | than or equal to - ``timezone.now()``. |
| | 407 | |
| | 408 | Testing our new view |
| | 409 | -------------------- |
| | 410 | |
| | 411 | Now you can satisfy yourself that this behaves as expected by firing up the |
| | 412 | runserver, loading the site in your browser, creating ``Polls`` with dates in |
| | 413 | the past and future, and checking that only those that have been published are |
| | 414 | listed. You don't want to have to do that *every single time you make any |
| | 415 | change that might affect this* - so let's also create a test, based on our |
| | 416 | shell session above. |
| | 417 | |
| | 418 | Add the following to ``polls/tests.py``:: |
| | 419 | |
| | 420 | # to work out URLs from the view we need urlresolvers.reverse |
| | 421 | from django.core.urlresolvers import reverse |
| | 422 | |
| | 423 | and we'll create a new class too:: |
| | 424 | |
| | 425 | # we'll put all the view tests in a class together |
| | 426 | class PollViewTests(TestCase): |
| | 427 | # setUp will set up our testing environment for each and every test method |
| | 428 | # in the class |
| | 429 | def setUp(self): |
| | 430 | # We'll set up some objects that we'll over and over again in our tests, |
| | 431 | # so that we don't have to repeat ourselves. Note that we don't save |
| | 432 | # them until we need them in the database - we do that in the test |
| | 433 | # methods themselves. |
| | 434 | |
| | 435 | # a Poll instance whose pub_date is 30 days ago |
| | 436 | self.beatles_poll = Poll( |
| | 437 | question="Who is your favourite Beatle?", |
| | 438 | pub_date=timezone.now() - datetime.timedelta(days=30) |
| | 439 | ) |
| | 440 | |
| | 441 | # a Poll instance whose pub_date is 30 in the future |
| | 442 | self.stones_poll = Poll( |
| | 443 | question="Who is your favourite Rolling Stone?", |
| | 444 | pub_date=timezone.now() + datetime.timedelta(days=30) |
| | 445 | ) |
| | 446 | |
| | 447 | def test_index_view_with_no_polls(self): |
| | 448 | # issue a GET request. |
| | 449 | response = self.client.get(reverse('polls:index')) |
| | 450 | # we should get a 200 OK |
| | 451 | self.assertEqual(response.status_code, 200) |
| | 452 | # the response should contain a message |
| | 453 | self.assertContains(response, "No polls are available.") |
| | 454 | # there should be nothing in the latest_poll_list |
| | 455 | self.assertEqual(len(response.context['latest_poll_list']), 0) |
| | 456 | |
| | 457 | def test_index_view_with_a_past_poll(self): |
| | 458 | self.beatles_poll.save() |
| | 459 | response = self.client.get(reverse('polls:index')) |
| | 460 | # the only item in the list should be beatles_poll |
| | 461 | self.assertEqual(len(response.context['latest_poll_list']), 1) |
| | 462 | self.assertEqual(response.context['latest_poll_list'][0], self.beatles_poll) |
| | 463 | |
| | 464 | def test_index_view_with_a_future_poll(self): |
| | 465 | self.stones_poll.save() |
| | 466 | response = self.client.get(reverse('polls:index')) |
| | 467 | # there should be no polls in the index |
| | 468 | # assertContains() allows us to check a number of things in one go |
| | 469 | self.assertContains(response, "No polls are available.", status_code=200) |
| | 470 | self.assertEqual(len(response.context['latest_poll_list']), 0) |
| | 471 | |
| | 472 | def test_index_view_with_future_poll_and_past_poll(self): |
| | 473 | self.beatles_poll.save() |
| | 474 | self.stones_poll.save() |
| | 475 | response = self.client.get(reverse('polls:index')) |
| | 476 | # the only item in the list should *still* be beatles_poll |
| | 477 | self.assertEqual(len(response.context['latest_poll_list']), 1) |
| | 478 | self.assertEqual(response.context['latest_poll_list'][0], self.beatles_poll) |
| | 479 | |
| | 480 | def test_index_view_with_two_past_polls(self): |
| | 481 | self.beatles_poll.save() |
| | 482 | # change the date on the stones_poll - put it in the past |
| | 483 | self.stones_poll.pub_date = timezone.now() - datetime.timedelta(days=5) |
| | 484 | self.stones_poll.save() |
| | 485 | response = self.client.get(reverse('polls:index')) |
| | 486 | # we should have 2 items now, stones_poll first and beatles_poll second |
| | 487 | self.assertEqual(len(response.context['latest_poll_list']), 2) |
| | 488 | self.assertEqual(response.context['latest_poll_list'][0], self.stones_poll) |
| | 489 | self.assertEqual(response.context['latest_poll_list'][1], self.beatles_poll) |
| | 490 | |
| | 491 | Let's look at some of these more closely. |
| | 492 | |
| | 493 | All the test methods run ``setUp()``, which creates the two ``Poll`` instances. |
| | 494 | It doesn't save them though; we only call ``save()`` when we need them. |
| | 495 | |
| | 496 | ``test_index_view_with_no_polls`` hasn't saved either of the ``Polls``, so we |
| | 497 | expect the poll index view load a page, but to return the message: "No polls are |
| | 498 | available.", and the ``latest_poll_list`` variable to be empty. |
| | 499 | |
| | 500 | In ``test_index_view_with_a_past_poll``, a poll has been saved to the database, |
| | 501 | so we *do* expect to find it in the list. |
| | 502 | |
| | 503 | In ``test_index_view_with_a_future_poll``, we save the other poll. The database |
| | 504 | is reset for each test method, so the first poll is no longer there, and so |
| | 505 | again the index shouldn't have any polls in it. Note that there is more than |
| | 506 | one ``assert`` method at our disposal; here we are using a new one, |
| | 507 | :meth:`~django.test.TestCase.assertContains()`. |
| | 508 | |
| | 509 | And so on. In effect, we are using the tests to tell a story of admin input |
| | 510 | and user experience on the site, and checking that at every state and for every |
| | 511 | new change in the state of the system, the expected results are published. |
| | 512 | |
| | 513 | Testing the ``DetailView`` |
| | 514 | -------------------------- |
| | 515 | |
| | 516 | What we have works well; however, even though future polls don't appear in the |
| | 517 | *index*, users can still reach them if they know or guess the right URL. So we |
| | 518 | need similar constraints in the ``DetailViews``, by adding:: |
| | 519 | |
| | 520 | queryset=Poll.objects.filter(pub_date__lte=timezone.now()) |
| | 521 | |
| | 522 | to them - for example:: |
| | 523 | |
| | 524 | url(r'^(?P<pk>\d+)/$', |
| | 525 | DetailView.as_view( |
| | 526 | queryset=Poll.objects.filter(pub_date__lte=timezone.now()), |
| | 527 | model=Poll, |
| | 528 | template_name='polls/detail.html'), |
| | 529 | name='detail'), |
| | 530 | |
| | 531 | and of course, we will add some tests, to check that a ``Poll`` whose |
| | 532 | ``pub_date`` is in the past can be displayed, and that one with a ``pub_date`` |
| | 533 | in the future is not:: |
| | 534 | |
| | 535 | class PollIndexDetailTests(TestCase): |
| | 536 | def setUp(self): |
| | 537 | # a Poll instance whose pub_date is 30 in the future |
| | 538 | self.stones_poll = Poll( |
| | 539 | question="Who is your favourite Rolling Stone?", |
| | 540 | pub_date=timezone.now() + datetime.timedelta(days=30) |
| | 541 | ) |
| | 542 | |
| | 543 | def test_detail_view_with_a_future_poll(self): |
| | 544 | self.stones_poll.save() |
| | 545 | response = self.client.get(reverse('polls:detail', args=(self.stones_poll.id,))) |
| | 546 | # should return a 404 not found |
| | 547 | self.assertEqual(response.status_code, 404) |
| | 548 | |
| | 549 | def test_detail_view_with_a_past_poll(self): |
| | 550 | # change the date on the stones_poll - put it in the past |
| | 551 | self.stones_poll.pub_date = timezone.now() - datetime.timedelta(days=5) |
| | 552 | self.stones_poll.save() |
| | 553 | response = self.client.get(reverse('polls:detail', args=(self.stones_poll.id,))) |
| | 554 | # now the same poll should return a 200 OK |
| | 555 | self.assertContains(response, self.stones_poll.question, status_code=200) |
| | 556 | |
| | 557 | Ideas for more tests |
| | 558 | -------------------- |
| | 559 | |
| | 560 | We ought to add similar ``queryset`` arguments to the other DetailView URLs, |
| | 561 | and create a new test class for each view. They'll be very similar to what we |
| | 562 | have just created; in fact there will be a lot of repetition. |
| | 563 | |
| | 564 | We could also improve our application in other ways, adding tests along the |
| | 565 | way. For example, it's silly that ``Polls`` can be published on the site that |
| | 566 | have no ``Choices``. So, our views could check for this, and exclude such |
| | 567 | ``Polls``. Our tests would create a ``Poll`` without ``Choices`` and then test |
| | 568 | that it's not published, as well as create a similar ``Poll`` *with* |
| | 569 | ``Choices``, and test that it *is* published. |
| | 570 | |
| | 571 | Perhaps logged-in admin users should be allowed to see unpublished ``Polls``, |
| | 572 | but not ordinary visitors. Again: whatever needs to be added to the software to |
| | 573 | accomplish this should be accompanied by a test, whether you write the test |
| | 574 | first and then make the code pass the test, or work out the logic in your code |
| | 575 | first and then write a test to prove it. |
| | 576 | |
| | 577 | At a certain point you are bound to look at your tests and wonder whether your |
| | 578 | code is suffering from test bloat, which brings us to: |
| | 579 | |
| | 580 | When testing, more is better |
| | 581 | ============================ |
| | 582 | |
| | 583 | It might seem that our tests are growing out of control. At this rate there will |
| | 584 | soon be more code in our tests than in our application, and the repetition |
| | 585 | is unaesthetic, compared to the elegant conciseness of the rest of our code. |
| | 586 | |
| | 587 | **It doesn't matter**. Let them grow. For the most part, you can write a test |
| | 588 | once and then forget about it. It will continue performing its useful function |
| | 589 | as you continue to develop your program. |
| | 590 | |
| | 591 | Sometimes tests will need to be updated. Suppose that we amend our views so that |
| | 592 | only ``Polls`` with ``Choices`` are published. In that case, many of our |
| | 593 | existing tests will fail - *telling us exactly which tests need to be amended to |
| | 594 | bring them up to date*, so to that extent tests help look after themselves. |
| | 595 | |
| | 596 | At worst, as you continue developing, you might find that you have some tests |
| | 597 | that are now redundant. Even that's not a problem; in testing redundancy is |
| | 598 | a *good* thing. |
| | 599 | |
| | 600 | As long as your tests are sensibly arranged, they won't become unmanageable. |
| | 601 | Good rules-of-thumb include having: |
| | 602 | |
| | 603 | * a separate ``TestClass`` for each model or view |
| | 604 | * a separate test method for each set of conditions you want to test |
| | 605 | * test method names that describe their function |
| | 606 | |
| | 607 | Further testing |
| | 608 | =============== |
| | 609 | |
| | 610 | This tutorial only introduces some of the basics of testing. There's a great |
| | 611 | deal more you can do, and a number of very useful tools at your disposal to |
| | 612 | achieve some very clever things. |
| | 613 | |
| | 614 | For example, while our tests here have covered some of the internal logic of a |
| | 615 | model and the way our views publish information, you can use an "in-browser" |
| | 616 | framework such as Selenium_ to test the way your HTML actually renders in a |
| | 617 | browser. These tools allow you to check not just the behavior of your Django |
| | 618 | code, but also, for example, of your JavaScript. It's quite something to see |
| | 619 | the tests launch a browser, and start interacting with your site, as if a human |
| | 620 | being were driving it! Django includes :class:`~django.test.LiveServerTestCase` |
| | 621 | to facilitate integration with tools like Selenium. |
| | 622 | |
| | 623 | If you have a complex application, you may want to run tests automatically |
| | 624 | with every commit for the purposes of `continuous integration`_, so that |
| | 625 | quality control is itself - at least partially - automated. |
| | 626 | |
| | 627 | :doc:`Testing Django applications </topics/testing>` has comprehensive |
| | 628 | information about testing. |
| | 629 | |
| | 630 | .. _Selenium: http://seleniumhq.org/ |
| | 631 | .. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration |
| | 632 | |
| | 633 | What's next? |
| | 634 | ============ |
| | 635 | |
| | 636 | The beginner tutorial ends here for the time being. In the meantime, you might |
| | 637 | want to check out some pointers on :doc:`where to go from here |
| | 638 | </intro/whatsnext>`. |
| | 639 | |
| | 640 | If you are familiar with Python packaging and interested in learning how to |
| | 641 | turn polls into a "reusable app", check out :doc:`Advanced tutorial: How to |
| | 642 | write reusable apps</intro/reusable-apps>`. |