Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lessons in Testing - EuroPython

Lessons in Testing - EuroPython

Talk given at EuroPython 2012

David Cramer

July 04, 2012
Tweet

More Decks by David Cramer

Other Decks in Technology

Transcript

  1. DISQUS sees a lot of tra c Google Analytics: May

    30 2012 - June 28 2012 Wednesday, July 4, 12
  2. Simple Feature: 10 lines of code from django.db import transaction

    class AutocommitMiddleware(object): def process_view(self, request, *a, **kw): if request.method == 'POST': enter_transaction_management(True) def process_response(self, request, response): leave_transaction_management() return response def process_exception(self, *a, **kw): leave_transaction_management() Wednesday, July 4, 12
  3. with precision testing class AutocommitMiddlewareTest: @mock.patch('enter_transaction_management') def test_something_precise(self, etm): request

    = WsgiHttpRequest() request.method = 'POST' middleware = AutocommitMiddleware() middleware.process_view(request) etm.assert_called_once_with(True) Wednesday, July 4, 12
  4. turns into 36 lines of tests import mock from mock_django.http

    import WsgiHttpRequest from django.http import HttpResponse from disqus.middleware.autocommit import AutocommitMiddleware from disqus.tests import DisqusTest class AutocommitMiddlewareTest(DisqusTest): @mock.patch('disqus.middleware.autocommit.transaction.enter_transaction_management') def test_changes_to_transactional_on_post(self, enter_transaction_management): request = WsgiHttpRequest() request.method = 'POST' middleware = AutocommitMiddleware() middleware.process_view(request, lambda: "", [], {}) enter_transaction_management.assert_called_once_with(True, using='default') @mock.patch('disqus.middleware.autocommit.transaction.enter_transaction_management') def test_leaves_autocommit_on_get(self, enter_transaction_management): request = WsgiHttpRequest() request.method = 'GET' middleware = AutocommitMiddleware() middleware.process_view(request, lambda: "", [], {}) self.assertFalse(enter_transaction_management.called) @mock.patch('disqus.middleware.autocommit.transaction.leave_transaction_management') def test_changes_to_autocommit_on_response(self, leave_transaction_management): request = WsgiHttpRequest() request.method = 'GET' response = HttpResponse() middleware = AutocommitMiddleware() middleware.process_response(request, response) leave_transaction_management.assert_called_once_with(using='default') @mock.patch('disqus.middleware.autocommit.transaction.leave_transaction_management') def test_changes_to_autocommit_on_exception(self, leave_transaction_management): request = WsgiHttpRequest() request.method = 'GET' exception = Exception() middleware = AutocommitMiddleware() middleware.process_exception(request, exception) leave_transaction_management.assert_called_once_with(using='default') Wednesday, July 4, 12
  5. class AutocommitMiddlewareTest: def setUp(self): assert 'AutocommitMiddleware' in INSTALLED_APPS def test_something_useful(self):

    with self.assertInTransaction(): # use Django’s integrated test client self.client.post('/') Wednesday, July 4, 12
  6. class TwitterTest: # This is not the Twitter API you

    are # looking for @mock.patch('twitter.Twitter.getresponse') def test_something_useful(self, gr): gr.return_value = TWITTER_RESPONSE res = Twitter().foo() assert res == [1, 2, 3] Wednesday, July 4, 12
  7. A lot of Software is Hard to Test (but yours

    doesn’t have to be) Wednesday, July 4, 12
  8. def static_media(request, path): """ Serve static files below a given

    point in the directory structure. """ from django.utils.http import http_date from django.views.static import was_modified_since import mimetypes import os.path import posixpath import stat import urllib document_root = os.path.join(settings.STATIC_ROOT) path = posixpath.normpath(urllib.unquote(path)) path = path.lstrip('/') newpath = '' for part in path.split('/'): if not part: # Strip empty path components. continue drive, part = os.path.splitdrive(part) head, part = os.path.split(part) if part in (os.curdir, os.pardir): # Strip '.' and '..' in path. continue newpath = os.path.join(newpath, part).replace('\\', '/') if newpath and path != newpath: return HttpResponseRedirect(newpath) fullpath = os.path.join(document_root, newpath) if os.path.isdir(fullpath): raise Http404("Directory indexes are not allowed here.") if not os.path.exists(fullpath): raise Http404('"%s" does not exist' % fullpath) # Respect the If-Modified-Since header. statobj = os.stat(fullpath) mimetype = mimetypes.guess_type(fullpath)[0] or 'application/octet-stream' if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): return HttpResponseNotModified(mimetype=mimetype) contents = open(fullpath, 'rb').read() response = HttpResponse(contents, mimetype=mimetype) response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) response["Content-Length"] = len(contents) return response Wednesday, July 4, 12
  9. def static_media(request, path): """ Serve static files below a given

    point in the directory structure. """ from django.utils.http import http_date from django.views.static import was_modified_since import mimetypes import os.path import posixpath import stat import urllib document_root = os.path.join(settings.STATIC_ROOT) path = posixpath.normpath(urllib.unquote(path)) path = path.lstrip('/') newpath = '' for part in path.split('/'): if not part: # Strip empty path components. continue drive, part = os.path.splitdrive(part) head, part = os.path.split(part) if part in (os.curdir, os.pardir): # Strip '.' and '..' in path. continue newpath = os.path.join(newpath, part).replace('\\', '/') if newpath and path != newpath: return HttpResponseRedirect(newpath) fullpath = os.path.join(document_root, newpath) if os.path.isdir(fullpath): raise Http404("Directory indexes are not allowed here.") if not os.path.exists(fullpath): raise Http404('"%s" does not exist' % fullpath) # Respect the If-Modified-Since header. statobj = os.stat(fullpath) mimetype = mimetypes.guess_type(fullpath)[0] or 'application/octet-stream' if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): return HttpResponseNotModified(mimetype=mimetype) contents = open(fullpath, 'rb').read() response = HttpResponse(contents, mimetype=mimetype) response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) response["Content-Length"] = len(contents) return response Wednesday, July 4, 12
  10. class AutocommitMiddlewareTest(DisqusTest): def test_changes_to_transactional_on_post(self): # adding a simple assert helper

    makes this # test much easier to write with self.assertInTransaction(): # changing this to an integration test # doesn’t require an understanding of # Django middleware internals self.client.post('/') Wednesday, July 4, 12
  11. tests !"" __init__.py !"" integration # !"" __init__.py # !""

    disqus # # !"" __init__.py # # !"" db # # # !"" __init__.py # # # !"" annotations # # # # !"" __init__.py # # # # !"" models.py # # # # %"" tests.py # # # !"" cachedbymodel # # # # !"" __init__.py # # # # !"" models.py # # # # %"" tests.py # # # !"" disqusmanager # # # # !"" __init__.py # # # # !"" models.py # # # # %"" tests.py # # # !"" iterablequeryset # # # # !"" __init__.py # # # # %"" tests.py Wednesday, July 4, 12
  12. # dump XML results nosetests --with-xunit # drop into a

    PDB on errors and failures nosetests --pdb --pdb-failure Wednesday, July 4, 12
  13. # use ``coverage run`` instead of ``python`` coverage run setup.py

    test # generate html reports coverage html --include=disqus/sexyapi/* # realize how terrible your tests are open htmlcov/index.html Wednesday, July 4, 12
  14. Selenium api/* Unit Integration disqus-web admin/* analytics/* JavaScript Package Simple

    Build Distribution embed/* forums/* db/* Wednesday, July 4, 12
  15. Obligatory Slide Full of Useful Links ๏ Sentry (exception reporting)

    github.com/dcramer/sentry ๏ Jenkins CI (continuous integration) jenkins-ci.org ๏ Phabricator (code review) phabricator.org ๏ Gargoyle (feature switches) github.com/disqus/gargoyle ๏ Mock mock.readthedocs.org ๏ Nose (test runner) nose.readthedocs.org ๏ nose-quickunit (test discovery) github.com/dcramer/nose-quickunit ๏ django-nose (Django integration) github.com/jbalogh/django-nose ๏ Coverage.py (code coverage) nedbatchelder.com/code/coverage/ Wednesday, July 4, 12