Permalink
Browse files

finish rewriting tute 3

  • Loading branch information...
1 parent b48ac50 commit 527991632cca0ed3dd1df902e84dc841d3c68781 @hjwp committed Apr 9, 2012
Showing with 250 additions and 76 deletions.
  1. +129 −0 mysite/fts/tests.py
  2. +3 −10 mysite/mysite/urls.py
  3. +9 −0 mysite/polls/templates/home.html
  4. +28 −0 mysite/polls/tests.py
  5. +10 −1 mysite/polls/views.py
  6. +71 −65 tutorial03.rst
View
129 mysite/fts/tests.py
@@ -81,3 +81,132 @@ def test_can_create_new_poll_via_admin_site(self):
"How awesome is Test-Driven Development?"
)
self.assertEquals(len(new_poll_links), 1)
+
+ # Satisfied, she goes back to sleep
+
+
+
+from collections import namedtuple
+
+PollInfo = namedtuple('PollInfo', ['question', 'choices'])
+POLL1 = PollInfo(
+ question="How awesome is Test-Driven Development?",
+ choices=[
+ 'Very awesome',
+ 'Quite awesome',
+ 'Moderately awesome',
+ ],
+)
+POLL2 = PollInfo(
+ question="Which workshop treat do you prefer?",
+ choices=[
+ 'Beer',
+ 'Pizza',
+ 'The Acquisition of Knowledge',
+ ],
+)
+
+
+
+class TestPolls(LiveServerTestCase):
+ fixtures = ['admin_user.json']
+
+ def setUp(self):
+ self.browser = webdriver.Firefox()
+
+ def tearDown(self):
+ self.browser.quit()
+
+ def _setup_polls_via_admin(self):
+ # Gertrude logs into the admin site
+ self.browser.get(self.live_server_url + '/admin/')
+ username_field = self.browser.find_element_by_name('username')
+ username_field.send_keys('admin')
+ password_field = self.browser.find_element_by_name('password')
+ password_field.send_keys('adm1n')
+ password_field.send_keys(Keys.RETURN)
+
+ # She has a number of polls to enter. For each one, she:
+ for poll_info in [POLL1, POLL2]:
+ # Follows the link to the Polls app, and adds a new Poll
+ self.browser.find_elements_by_link_text('Polls')[1].click()
+ self.browser.find_element_by_link_text('Add poll').click()
+
+ # Enters its name, and uses the 'today' and 'now' buttons to set
+ # the publish date
+ question_field = self.browser.find_element_by_name('question')
+ question_field.send_keys(poll_info.question)
+ self.browser.find_element_by_link_text('Today').click()
+ self.browser.find_element_by_link_text('Now').click()
+
+ # Sees she can enter choices for the Poll on this same page,
+ # so she does
+ for i, choice_text in enumerate(poll_info.choices):
+ choice_field = self.browser.find_element_by_name('choice_set-%d-choice' % i)
+ choice_field.send_keys(choice_text)
+
+ # Saves her new poll
+ save_button = self.browser.find_element_by_css_selector("input[value='Save']")
+ save_button.click()
+
+ # Is returned to the "Polls" listing, where she can see her
+ # new poll, listed as a clickable link by its name
+ new_poll_links = self.browser.find_elements_by_link_text(
+ poll_info.question
+ )
+ self.assertEquals(len(new_poll_links), 1)
+
+ # She goes back to the root of the admin site
+ self.browser.get(self.live_server_url + '/admin/')
+
+ # She logs out of the admin site
+ self.browser.find_element_by_link_text('Log out').click()
+
+
+ def test_voting_on_a_new_poll(self):
+ # First, Gertrude the administrator logs into the admin site and
+ # creates a couple of new Polls, and their response choices
+ self._setup_polls_via_admin()
+
+ # Now, Herbert the regular user goes to the homepage of the site. He
+ # sees a list of polls.
+ self.browser.get(self.live_server_url)
+ heading = self.browser.find_element_by_tag_name('h1')
+ self.assertEquals(heading.text, 'Polls')
+
+ # He clicks on the link to the first Poll, which is called
+ # 'How awesome is test-driven development?'
+ first_poll_title = 'How awesome is Test-Driven Development?'
+ self.browser.find_element_by_link_text(first_poll_title).click()
+
+ # He is taken to a poll 'results' page, which says
+ # "no-one has voted on this poll yet"
+ main_heading = self.browser.find_element_by_tag_name('h1')
+ self.assertEquals(main_heading.text, 'Poll Results')
+ sub_heading = self.browser.find_element_by_tag_name('h2')
+ self.assertEquals(sub_heading.text, first_poll_title)
+ body = self.browser.find_element_by_tag_name('body')
+ self.assertIn('No-one has voted on this poll yet', body.text)
+
+ self.fail('TODO')
+
+ # He clicks on the link to the first Poll, which is called
+ # 'How awesome is test-driven development?'
+
+ # He is taken to a poll 'results' page, which says
+ # "no-one has voted on this poll yet"
+
+ # He also sees a form, which offers him several choices.
+ # He decided to select "very awesome"
+
+ # He clicks 'submit'
+
+ # The page refreshes, and he sees that his choice
+ # has updated the results. they now say
+ # "100 %: very awesome".
+
+ # The page also says "1 votes"
+
+ # Satisfied, he goes back to sleep
+
+
View
13 mysite/mysite/urls.py
@@ -1,17 +1,10 @@
from django.conf.urls import patterns, include, url
-
-# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
- # Examples:
- # url(r'^$', 'mysite.views.home', name='home'),
- # url(r'^mysite/', include('mysite.foo.urls')),
-
- # Uncomment the admin/doc line below to enable admin documentation:
- # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
-
- # Uncomment the next line to enable the admin:
+ url(r'^$', 'polls.views.home'),
+ url(r'^poll/(\d+)/$', 'polls.views.poll'),
url(r'^admin/', include(admin.site.urls)),
)
+
View
9 mysite/polls/templates/home.html
@@ -0,0 +1,9 @@
+<html>
+ <body>
+ <h1>Polls</h1>
+ {% for poll in polls %}
+ <p><a href="{% url polls.views.poll poll.id %}">{{ poll.question }}</a></p>
+ {% endfor %}
+ </body>
+</html>
+
View
28 mysite/polls/tests.py
@@ -1,3 +1,4 @@
+from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import timezone
from polls.models import Choice, Poll
@@ -77,3 +78,30 @@ def test_choice_defaults(self):
choice = Choice()
self.assertEquals(choice.votes, 0)
+class TestHomePageView(TestCase):
+
+ def test_root_url_shows_links_to_all_polls(self):
+ # set up some polls
+ poll1 = Poll(question='6 times 7', pub_date=timezone.now())
+ poll1.save()
+ poll2 = Poll(question='life, the universe and everything', pub_date=timezone.now())
+ poll2.save()
+
+ response = self.client.get('/')
+
+ template_names_used = [t.name for t in response.templates]
+ self.assertIn('home.html', template_names_used)
+
+ # check we've passed the polls to the template
+ polls_in_context = response.context['polls']
+ self.assertEquals(list(polls_in_context), [poll1, poll2])
+
+ # check the poll names appear on the page
+ self.assertIn(poll1.question, response.content)
+ self.assertIn(poll2.question, response.content)
+
+ # check the page also contains the urls to individual polls pages
+ poll1_url = reverse('polls.views.poll', args=[poll1.id,])
+ self.assertIn(poll1_url, response.content)
+ poll2_url = reverse('polls.views.poll', args=[poll2.id,])
+ self.assertIn(poll2_url, response.content)
View
11 mysite/polls/views.py
@@ -1 +1,10 @@
-# Create your views here.
+from polls.models import Poll
+from django.shortcuts import render
+
+def home(request):
+ context = {'polls': Poll.objects.all()}
+ return render(request, 'home.html', context)
+
+
+def poll():
+ pass
View
136 tutorial03.rst
@@ -17,15 +17,24 @@ Writing the FT as comments
Let's start by writing out our FT as human-readable comments, which describe the user's actions, and the expected behaviour of the site
-Create a new file for it called ``fts/test_polls.py``.
+Create a new class inside ``fts/tests.py``.
.. sourcecode:: python
- :filename: mysite/fts/test_polls.py
+ :filename: mysite/fts/tests.py
- from functional_tests import FunctionalTest, ROOT
- from selenium.webdriver.common.keys import Keys
+ [...]
+ self.assertEquals(len(new_poll_links), 1)
- class TestPolls(FunctionalTest):
+ # Satisfied, she goes back to sleep
+
+ class TestPolls(LiveServerTestCase):
+ fixtures = ['admin_user.json']
+
+ def setUp(self):
+ self.browser = webdriver.Firefox()
+
+ def tearDown(self):
+ self.browser.quit()
def test_voting_on_a_new_poll(self):
# First, Gertrude the administrator logs into the admin site and
@@ -65,10 +74,13 @@ You'll see I've changed things slightly, because in the admin test we entered ju
http://stackoverflow.com/questions/2970608/what-are-named-tuples-in-python)
.. sourcecode:: python
- :filename: mysite/fts/test_polls.py
+ :filename: mysite/fts/tests.py
+
+ [...]
+ self.assertEquals(len(new_poll_links), 1)
+
+ # Satisfied, she goes back to sleep
- from functional_tests import FunctionalTest, ROOT
- from selenium.webdriver.common.keys import Keys
from collections import namedtuple
PollInfo = namedtuple('PollInfo', ['question', 'choices'])
@@ -89,10 +101,18 @@ http://stackoverflow.com/questions/2970608/what-are-named-tuples-in-python)
],
)
- class TestPolls(FunctionalTest):
+ class TestPolls(LiveServerTestCase):
+ fixtures = ['admin_user.json']
+
+ def setUp(self):
+ self.browser = webdriver.Firefox()
+
+ def tearDown(self):
+ self.browser.quit()
+
def _setup_polls_via_admin(self):
# Gertrude logs into the admin site
- self.browser.get(ROOT + '/admin/')
+ self.browser.get(self.live_server_url + '/admin/')
username_field = self.browser.find_element_by_name('username')
username_field.send_keys('admin')
password_field = self.browser.find_element_by_name('password')
@@ -130,7 +150,7 @@ http://stackoverflow.com/questions/2970608/what-are-named-tuples-in-python)
self.assertEquals(len(new_poll_links), 1)
# She goes back to the root of the admin site
- self.browser.get(ROOT + '/admin/')
+ self.browser.get(self.live_server_url + '/admin/')
# She logs out of the admin site
self.browser.find_element_by_link_text('Log out').click()
@@ -142,15 +162,16 @@ http://stackoverflow.com/questions/2970608/what-are-named-tuples-in-python)
self._setup_polls_via_admin()
self.fail('TODO')
+ # Now, Herbert the regular user goes to the homepage of the site. He
Now, if you try running that test, you should see selenium run through and enter the two polls, and then exit with the "TODO"::
======================================================================
- FAIL: test_voting_on_a_new_poll (test_polls.TestPolls)
+ FAIL: test_voting_on_a_new_poll (tests.TestPolls)
----------------------------------------------------------------------
Traceback (most recent call last):
- File "/home/harry/workspace/tddjango_site/source/mysite/fts/test_polls.py", line 76, in test_voting_on_a_new_poll
+ File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 76, in test_voting_on_a_new_poll
self.fail('TODO')
AssertionError: TODO
----------------------------------------------------------------------
@@ -166,7 +187,7 @@ Let's write the exciting bit of our test, where Herbert the normal user opens up
.. sourcecode:: python
- :filename: mysite/fts/test_polls.py
+ :filename: mysite/fts/tests.py
def test_voting_on_a_new_poll(self):
# First, Gertrude the administrator logs into the admin site and
@@ -175,7 +196,7 @@ Let's write the exciting bit of our test, where Herbert the normal user opens up
# Now, Herbert the regular user goes to the homepage of the site. He
# sees a list of polls.
- self.browser.get(ROOT)
+ self.browser.get(self.live_server_url)
heading = self.browser.find_element_by_tag_name('h1')
self.assertEquals(heading.text, 'Polls')
@@ -195,26 +216,21 @@ Let's write the exciting bit of our test, where Herbert the normal user opens up
self.fail('TODO')
+ # He clicks on the link to the first Poll, which is called
+ [...]
+
We've started with the first bit, where Herbert goes to the main page of the site, we check that he can see a Poll there, and that he can click on it. Then we look for the default 'no votes yet' message on the next page.
Let's run that, and see where we get::
- ======================================================================
- FAIL: test_voting_on_a_new_poll (test_polls.TestPolls)
- ----------------------------------------------------------------------
- Traceback (most recent call last):
- File "/home/harry/workspace/Test-Driven-Django-Tutorial/mysite/fts/test_polls.py", line 57, in test_voting_on_a_new_poll
- self.assertEquals(heading.text, 'Polls')
- AssertionError: u'Page not found (404)' != 'Polls'
- ----------------------------------------------------------------------
- Ran 2 tests in 19.772s
+ AssertionError: u'Page not found (404)' != 'PollsNoSuchElementException: Message: u'Unable to locate element: {"method":"tag name","selector":"h1"}'
URLS and view functions, and the Django Test Client
---------------------------------------------------
-The FT is telling us that going to the `ROOT` url (/) produces a 404. We need to tell Django what kind of web page to return for the root of our site - the home page if you like.
+The FT is telling us that going to the root url (/) produces an error. We need to tell Django what kind of web page to return for the root of our site - the home page if you like.
Django uses a file called ``urls.py``, to route visitors to the python function that will deal with producing a response for them. These functions are called `views` in Django terminology, and they live in ``views.py``.
@@ -230,7 +246,6 @@ We'll create a new class to test our home page view:
.. sourcecode:: python
:filename: mysite/polls/tests.py
- from django.test.client import Client
[...]
class TestHomePageView(TestCase):
@@ -241,16 +256,14 @@ We'll create a new class to test our home page view:
poll2 = Poll(question='life, the universe and everything', pub_date=timezone.now())
poll2.save()
- client = Client()
- response = client.get('/')
+ response = self.client.get('/')
self.assertIn(poll1.question, response.content)
self.assertIn(poll2.question, response.content)
Don't forget the import at the top!
-Now, our first run of the tests will probably complain of a with ``TemplateDoesNotExist: 404.html``. Django wants us to create a template for our "404 error" page. We'll come back to that later. For now, let's make the
-``/`` url return a real HTTP response.
+Now, our first run of the tests will probably complain of a with ``TemplateDoesNotExist: 404.html``. Django wants us to create a template for our "404 error" page. We'll come back to that later. For now, let's make the ``/`` url return a real HTTP response.
First we'll create a dummy view in ``views.py``:
@@ -263,7 +276,7 @@ First we'll create a dummy view in ``views.py``:
Now let's hook up this view inside ``urls.py``:
.. sourcecode:: python
- :filename: mysite/polls/urls.py
+ :filename: mysite/mysite/urls.py
from django.conf.urls import patterns, include, url
from django.contrib import admin
@@ -277,7 +290,7 @@ Now let's hook up this view inside ``urls.py``:
``urls.py`` maps urls (specified as regular expressions) to views. I've used dotted-string notation to specify the name of the view, but you could also use the actual view, like this:
.. sourcecode:: python
- :filename: mysite/polls/urls.py
+ :filename: mysite/mysite/urls.py
from polls.views import home
urlpatterns = patterns('',
@@ -365,10 +378,10 @@ So, rather than anticipate what we might want to put in our HttpResponse, let's
python functional_tests.py
======================================================================
- ERROR: test_voting_on_a_new_poll (test_polls.TestPolls)
+ ERROR: test_voting_on_a_new_poll (tests.TestPolls)
----------------------------------------------------------------------
Traceback (most recent call last):
- File "/home/harry/workspace/tddjango_site/source/mysite/fts/test_polls.py", line 57, in test_voting_on_a_new_poll
+ File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 57, in test_voting_on_a_new_poll
heading = self.browser.find_element_by_tag_name('h1')
File "/usr/local/lib/python2.7/dist-packages/selenium/webdriver/remote/webdriver.py", line 306, in find_element_by_tag_name
return self.find_element(by=By.TAG_NAME, value=name)
@@ -399,8 +412,7 @@ The Django TestCase lets us check whether a response was rendered using a templa
poll2 = Poll(question='life, the universe and everything', pub_date=timezone.now())
poll2.save()
- client = Client()
- response = client.get('/')
+ response = self.client.get('/')
# check we've used the right template
self.assertTemplateUsed(response, 'home.html')
@@ -433,27 +445,29 @@ That should give us a folder structure like this::
.
|-- database.sqlite
- |-- ft_database.sqlite
|-- fts
+ | |-- fixtures
+ | | `-- admin_user.json
| |-- __init__.py
- | |-- test_admin.py
- | `-- test_polls.py
- |-- functional_tests.py
+ | |-- models.py
+ | |-- test_polls.py
+ | |-- tests.py
+ | `-- views.py
|-- manage.py
|-- mysite
| |-- __init__.py
- | |-- settings_for_fts.py
| |-- settings.py
| |-- urls.py
| `-- wsgi.py
`-- polls
- |-- __init__.py
|-- admin.py
+ |-- __init__.py
|-- models.py
|-- templates
| `-- home.html
|-- tests.py
- `-- views.py
+ `-- views.py
+
Edit ``home.html`` with your favourite editor,
@@ -503,8 +517,7 @@ Looking at the template code, you can see that we want to iterate through a vari
.. sourcecode:: python
:filename: mysite/polls/tests.py
- client = Client()
- response = client.get('/')
+ response = self.client.get('/')
# check we've used the right template
self.assertTemplateUsed(response, 'home.html')
@@ -591,10 +604,10 @@ What do the FTs say now?::
python functional_tests.py
======================================================================
- ERROR: test_voting_on_a_new_poll (test_polls.TestPolls)
+ ERROR: test_voting_on_a_new_poll (tests.TestPolls)
----------------------------------------------------------------------
Traceback (most recent call last):
- File "/home/harry/workspace/tddjango_site/source/mysite/fts/test_polls.py", line 62, in test_voting_on_a_new_poll
+ File "/home/harry/workspace/tddjango_site/source/mysite/fts/tests.py", line 62, in test_voting_on_a_new_poll
self.browser.find_element_by_link_text('How awesome is Test-Driven Development?').click()
File "/usr/local/lib/python2.7/dist-packages/selenium/webdriver/remote/webdriver.py", line 234, in find_element_by_link_text
return self.find_element(by=By.LINK_TEXT, value=link_text)
@@ -668,8 +681,7 @@ Let's use the ``reverse`` function in our tests. Its first argument is the name
poll2 = Poll(question='life, the universe and everything', pub_date=timezone.now())
poll2.save()
- client = Client()
- response = client.get('/')
+ response = self.client.get('/')
template_names_used = [t.name for t in response.templates]
self.assertIn('home.html', template_names_used)
@@ -712,12 +724,12 @@ Capturing parameters from URLs
In ``urls.py``:
.. sourcecode:: python
- :filename: mysite/polls/urls.py
+ :filename: mysite/mysite/urls.py
urlpatterns = patterns('',
- (r'^$', 'polls.views.home'),
- (r'^poll/(\d+)/$', 'polls.views.poll'),
- (r'^admin/', include(admin.site.urls)),
+ url(r'^$', 'polls.views.home'),
+ url(r'^poll/(\d+)/$', 'polls.views.poll'),
+ url(r'^admin/', include(admin.site.urls)),
)
Our new line defines a set of urls that start with `poll/`, then a number made up of one or more digits - the matching group ``(\d+)``. When a url has a matching group, the captured contents are passed to the view as arguments. So, if you look back at what the unit test was last complaining about, we should have fixed its problem: we've now created a url that references ``'polls.views.poll'`` and which is capable of taking an argument of ``1``.
@@ -743,7 +755,7 @@ The templates don't include the hyperlinks yet. Let's add them:
<body>
<h1>Polls</h1>
{% for poll in polls %}
- <p><a href="{% url mysite.polls.views.poll poll.id %}">{{ poll.question }}</a></p>
+ <p><a href="{% url polls.views.poll poll.id %}">{{ poll.question }}</a></p>
{% endfor %}
</body>
</html>
@@ -802,11 +814,12 @@ Notice the call to ``{% url %}``, which works almost exactly like ``reverse``.
Phew. A long traceback, but basically all it's saying is that we need at least a placeholder for our new "poll" view in ``views.py``. Let's add that now:
.. sourcecode:: python
- :filename: mysite/polls/urls.py
+ :filename: mysite/mysite/urls.py
def home(request):
context = {'polls': Poll.objects.all()}
return render(request, 'home.html', context)
+
def poll():
pass
@@ -822,14 +835,7 @@ And run the unit tests again::
What about the functional tests?::
- ======================================================================
- FAIL: test_voting_on_a_new_poll (test_polls.TestPolls)
- ----------------------------------------------------------------------
- Traceback (most recent call last):
- File "/home/harry/workspace/tddjango_site/source/mysite/fts/test_polls.py", line 67, in test_voting_on_a_new_poll
- self.assertEquals(heading.text, 'Poll Results')
- AssertionError: u'TypeError at /poll/1/' != 'Poll Results'
- ----------------------------------------------------------------------
- Ran 2 tests in 25.927s
+ NoSuchElementException: Message: u'Unable to locate element: {"method":"tag name","selector":"h1"}'
+
-Looks like it's time to start implementing our `poll` view, which aims to show information about an individual poll... But for this, you'll have to tune in next week!
+Well, they get past the main page, but they fall over when they try to look at an individual poll. Looks like it's time to start implementing our `poll` view, which aims to show information about a particular poll... But for this, you'll have to tune in next week!

0 comments on commit 5279916

Please sign in to comment.