Django has a nice support for unit and functional testing; however, its django.test.client.Client class does not support PUT and DELETE requests, which might be useful if, like me, you’re doing some kind of REST implementation using that framework. There’s an open ticket about it, but for the time being, here’s my wrapper that supports those methods as well as GET and POST:
[source:python]
from cStringIO import StringIO
from django.test.client import Client as DjangoClient, encode_multipart
from django.utils.http import urlencode
import base64
import md5
class Client(DjangoClient):
“”"
Wrapper and drop-in replacement around Django’s own test
“Client” class, providing PUT, DELETE and OPTIONS support, as well as
HTTP Basic + Digest Authentication support.
NOTE: the django.test.client.Client does not directly support
PUT, DELETE or OPTIONS requests so we’re using the “request()”
method directly… there’s an open ticket about it:
http://code.djangoproject.com/ticket/5888
“”"
auth = { }
def http_basic_login(self, username, password):
base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
self.auth = { "HTTP_AUTHORIZATION": "Basic %s" % base64string }
def http_digest_login(self, method, url, params, response, username, password):
(authmeth, auth) = response['WWW-Authenticate'].split(" ", 1)
if authmeth.lower() != 'digest':
return
amap = {}
for itm in auth.split(", "):
(k, v) = [s.strip() for s in itm.split("=", 1)]
amap[k] = v.replace('"', '')
try:
realm = amap['realm']
qop = amap.get('qop', '')
nonce = amap['nonce']
opaque = amap['opaque']
except:
return
cnonce = "01b6730aae57c007"
nc = "00000001"
query_string = "&".join(["=".join(item) for item in zip(params.keys(), params.values())])
uri = url + "?" + query_string
ha1 = md5.md5('%s:%s:%s' % (username, realm, password)).hexdigest()
ha2 = md5.md5('%s:%s' % (method, uri)).hexdigest()
if qop:
chk = "%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)
else:
chk = "%s:%s:%s" % (ha1, nonce, ha2)
response = md5.md5(chk).hexdigest()
self.auth = {
"HTTP_AUTHORIZATION": 'Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", opaque="%s", qop=auth, nc=%s, cnonce="%s"' % (username, realm, nonce, uri, response, opaque, nc, cnonce),
}
def http_logout(self):
self.auth = {}
def pre_request(self, url, data):
r = {
'CONTENT_LENGTH': None,
'CONTENT_TYPE': 'text/html; charset=utf-8',
"HTTP_USER_AGENT": "Django Unit Test HTTP Client",
'PATH_INFO': url,
'QUERY_STRING': urlencode(data, doseq=True),
}
r.update(self.auth)
return r
def get(self, url, data, **extra):
r = {}
r.update(extra)
r.update(self.auth)
return DjangoClient.get(self, url, data, **r)
def post(self, url, data, **extra):
r = {}
r.update(extra)
r.update(self.auth)
return DjangoClient.post(self, url, data, **r)
def delete(self, url, data):
r = self.pre_request(url, data)
r["REQUEST_METHOD"] = "DELETE"
return self.request(**r)
def options(self, url, data):
r = self.pre_request(url, data)
r["REQUEST_METHOD"] = "OPTIONS"
return self.request(**r)
def put(self, url, data, form):
BOUNDARY = 'BoUnDaRyStRiNg'
MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
encoded = encode_multipart(BOUNDARY, form)
r = self.pre_request(url, data)
r.update({
'CONTENT_LENGTH': len(encoded),
'CONTENT_TYPE': MULTIPART_CONTENT,
'REQUEST_METHOD': 'PUT',
'wsgi.input': StringIO(encoded),
})
return self.request(**r)
def hello(self, url, data, **extra):
"""Sends a fake 'HELLO' request which returns a 405 answer :)"""
r = self.pre_request(url, data)
r["REQUEST_METHOD"] = "HELLO"
r.update(extra)
return self.request(**r)
[/source]
Hope it helps! Of course you could extend this to support OPTIONS, HEAD or other HTTP methods you could find in the specification.
Update, 2008-03-05: Following Yoan’s comment below, I’ve DRYed the code a bit. Neat.
Update, 2008-03-11: I’ve added HTTP Basic Authentication support to the class (and changed the post title accordingly).
Update, 2008-03-13: Another modification: now the client supports HTTP Digest Authentication (Yay! To use it, make a first call to your server and then pass the response as a parameter to the http_digest_login method), plus support for the OPTIONS verb, plus another method which sends a method with a “HELLO” verb, which of course does not exist… and which will (normally) return a 405 response!