diff -r 70fcef3980a0 django/test/testcases.py
--- a/django/test/testcases.py	Thu Jun 12 22:24:20 2008 -0400
+++ b/django/test/testcases.py	Thu Jun 12 23:31:26 2008 -0400
@@ -1,6 +1,7 @@ import re
 import re
 import unittest
 from urlparse import urlsplit, urlunsplit
+from xml.dom.minidom import parseString, Node
 
 from django.http import QueryDict
 from django.db import transaction
@@ -8,6 +9,7 @@ from django.core.management import call_
 from django.core.management import call_command
 from django.test import _doctest as doctest
 from django.test.client import Client
+from django.utils import simplejson
 
 normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
 
@@ -25,15 +27,132 @@ def to_list(value):
 
 class OutputChecker(doctest.OutputChecker):
     def check_output(self, want, got, optionflags):
-        ok = doctest.OutputChecker.check_output(self, want, got, optionflags)
+        checks = (self.check_output_default,
+                  self.check_output_long,
+                  self.check_output_xml,
+                  self.check_output_json)
+        for check in checks:
+            if check(want, got, optionflags):
+                return True
+        return False
 
+    def check_output_default(self, want, got, optionflags):
+        return doctest.OutputChecker.check_output(self, want, got, optionflags)
+
+    def check_output_long(self, want, got, optionflags):
         # Doctest does an exact string comparison of output, which means long
         # integers aren't equal to normal integers ("22L" vs. "22"). The
         # following code normalizes long integers so that they equal normal
         # integers.
-        if not ok:
-            return normalize_long_ints(want) == normalize_long_ints(got)
-        return ok
+        return normalize_long_ints(want) == normalize_long_ints(got)
+
+    def check_output_xml(self, want, got, optionsflags):
+        # Tries to do a 'xml-comparision' of want and got.  Plan string
+        # comparision doesn't always work, because, for example, attribute
+        # ordering should not be important.
+        #
+        # Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py
+
+        # We use this to distinguish repr()s from elements:
+        _repr_re = re.compile(r'^<[^>]+ (at|object) ')
+
+        _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+')
+        def norm_whitespace(v):
+            return _norm_whitespace_re.sub(' ', v)
+
+        def looks_like_markup(s):
+            s = s.strip()
+            return (s.startswith('<')
+                    and not _repr_re.search(s))
+
+        def child_text(element):
+            return ''.join([c.data for c in element.childNodes
+                            if c.nodeType == Node.TEXT_NODE])
+
+        def children(element):
+            return [c for c in element.childNodes
+                    if c.nodeType == Node.ELEMENT_NODE]
+
+        def norm_child_text(element):
+            return norm_whitespace(child_text(element))
+
+        def attrs_dict(element):
+            return dict(element.attributes.items())
+
+        def check_element(want_element, got_element):
+            if want_element.tagName != got_element.tagName:
+                return False
+            if norm_child_text(want_element) != norm_child_text(got_element):
+                return False
+            if attrs_dict(want_element) != attrs_dict(got_element):
+                return False
+            want_children = children(want_element)
+            got_children = children(got_element)
+            if len(want_children) != len(got_children):
+                return False
+            for want, got in zip(want_children, got_children):
+                if not check_element(want, got):
+                    return False
+            return True
+
+        want, got = self._strip_quotes(want, got)
+        if not looks_like_markup(want):
+            return False
+
+        # Wrapper to support XML fragments
+        wrapper = u"<root>%s</root>"
+        try:
+            want_root = parseString(wrapper % want).firstChild
+            got_root = parseString(wrapper % got).firstChild
+        except:
+            return False
+        return check_element(want_root, got_root)
+
+    def check_output_json(self, want, got, optionsflags):
+        # Tries do compare want and got as if they were JSON-encoded data
+        want, got = self._strip_quotes(want, got)
+        try:
+            want_json = simplejson.loads(want)
+            got_json = simplejson.loads(got)
+        except:
+            return False
+        return  want_json == got_json
+
+    def _strip_quotes(self, want, got):
+        """
+        Strip quotes of doctests output values:
+
+        >>> o = OutputChecker()
+        >>> o._strip_quotes("'foo'")
+        "foo"
+        >>> o._strip_quotes('"foo"')
+        "foo"
+        >>> o._strip_quotes("u'foo'")
+        "foo"
+        >>> o._strip_quotes('u"foo"')
+        "foo"
+        """
+        def is_quoted_string(s):
+            s = s.strip()
+            return (len(s) >= 2
+                    and s[0] == s[-1]
+                    and s[0] in ('"', "'"))
+
+        def is_quoted_unicode(s):
+            s = s.strip()
+            return (len(s) >= 3
+                    and s[0] == 'u'
+                    and s[1] == s[-1]
+                    and s[1] in ('"', "'"))
+
+        if is_quoted_string(want) and is_quoted_string(got):
+            want = want.strip()[1:-1]
+            got = got.strip()[1:-1]
+        elif is_quoted_unicode(want) and is_quoted_unicode(got):
+            want = want.strip()[2:-1]
+            got = got.strip()[2:-1]
+        return want, got
+
 
 class DocTestRunner(doctest.DocTestRunner):
     def __init__(self, *args, **kwargs):
