Based on lots of comments and other stuff on the “Internets”, I have started using Django-Webtest. Webtest inherits from Django TestCase. It allows you to interact with forms more like the user does. For example you can select an option from an HTML Select tag. I am still trying to decide if this extra functionality is worth having to deal with another layer of documentation. Especially since that layer is sparse.
Anyway, these are my notes for getting things done with Webtest. This post is not meant to be a tutorial or replace the docs. Rather its just stuff I have found useful and hard to find in the docs.
Viewing the Page in a Browser
Let’s say you are testing a form and things are not going as planned. Sometimes the easiest way to solve the problem is to view the page. Here is how:
form = self.app.get(reverse('login')).form
form['email'] = user.email
form['password'] = self.test_password
response = form.submit().follow()
The last line is where the magic is.
Finding Stuff on the Page
Webtest provides the Beautiful Soup parsed HTML in the .html attribute of a response. One way this is useful is to assert things about the content of a page. For example:
response = self.app.get(reverse('my_url', args=[obj.id]), user='jim')
delete_button = response.html.find('button', id='delete_button')
Checking the URL After Redirect
Sure, you can use status code. But what if you want to know where you were redirected? Use this Django assertion:
response = form.submit()
response = self.app.get(response.url, user='test') # do the redirect
Occasionally this does not work. For some reason, the port gets inserted in the response.url, while its not in reverse(). I have seen this problem occur with some assertRedirects and not others in the same test method. Here is one way to get around this:
from urlparse import urlparse
def my_assertRedirects(self, response, expected_url):
parts = urlparse(response.url)
I am using django-crispy forms. When I make an InlineCheckBoxes of a Django MultipleChoiceField, I end up with several HTML checkbox tags with the same name. Hence the usual method for assigning a value does not work. Here is something that does work:
form.set('my_checkbox_name', True, index=1)
where the index keyword specifies which checkbox with name “my_checkbox_name”.
Handling Form Errors
One way to handle form errors is to look at the response_status. For most of my forms, a status of 200 is returned when there is an error. While a 302 (redirect) is returned when there are no errors. Although this works, it feels like a pretty blunt instrument for handling form errors.
One interesting fact about WebTest is it inherits from Django TestCase. What his means is that some of the functionality of TestCase still works. For example, you can submit a form using the post method of the Django Client class.
For error handling, you can still use:
response = form.submit('save')
self.assertFormError(response, 'form', 'user_type', u'This field is required.')
or for non field errors:
response = response.form.submit('save')
self.assertFormError(response, 'form', None, ['an error message', 'another error message'])
Note that the word form is a string. It is the name of the form in the context dict that you send to render. Here are the docs on assertFormError.
What if you are getting a form error, but you are not sure what error it is? The place to start looking is in the response.context list.
Lets say you have a form that redirects after it is successfully submitted. If you submit it like this:
response = form.submit('save')
In the browser, you will see a blank page. What is going on here is Webtest did not automatically go to the page you are redirecting to. To have it go to that page, do this:
response = form.submit('save').follow()
ModelForm Initial Values
This one is baffling. If you pass initial values to a ModelForm using the initial keyword, they do not appear in the form! How do I know this? If I run the code outside of testing, they appear. If I run the code in testing and do showbrowser() they are not there. If I set break points during testing, I can see that the “initial” keyword has the correct values.
HTTP Forbidden (403)
With the systems I design, it is often necessary to limit access to views based on attributes of the user. Thus a big part of testing is making sure users cannot view things they are not allowed to view.
You would think this would be a simple test:
response = self.app.get(reverse('my_view'))
But if you do that you get an error:
AppError: Bad response: 403 FORBIDDEN
Instead try this:
response = self.app.get(reverse('my_view'), status=403)
If the response status is not 403, an error will be raised, which is exactly what you want.
Clicking a Link
If there is a link on your page (<a></a>) the webtest response object has a method called “click” which lets you click on the link. Seems easy enough. You can send regular expression patterns or strings to select the link you need.
The gottcha is when the uses “onclick” as its action. When the method is searching for the link, it also checks to see what the tags href attribute is. If there is no href attribute, then the link is skipped even if it matches the linkid.
Use the Verbose Keyword
Several methods accept the keyword “verbose”. Use it to speed up debugging.
The good news is you can inspect session variables by looking at:
The bad news is I cannot figure out how to set a session variable inside a test. It seems that writing to self.app.session does not work. WebTest uses a special backend to handle auth. It seems that is not connected to the normal session processing. If you know how to do this, let me know!
Ugh… this worse than I thought. For some mysterious reason, the session vars are not carrying over between views. I am solving this by switching to Django TestCase.