Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Django’s nasal passage

4,937 views

Published on

Published in: Technology, Business
  • Be the first to comment

Django’s nasal passage

  1. Nasaldjango’s Passage by @ErikRose Scale up your project. Streamline your tests.
  2. OK! 1729 tests, 0 failures, 0 errors, 13 skips in 436.4s
  3. Django Testing Pain Points
  4. Django Testing Pain Points • Crowded
  5. Django Testing Pain Points • Crowded • Slow
  6. Django Testing Pain Points • Crowded • Slow • Overbroad
  7. Django Testing Pain Points • Crowded INSTALLED_APPS = [... • Slow django.contrib.admin, django.contrib.admindocs, django.contrib.auth, • Overbroad django.contrib.contenttypes, django.contrib.humanize, django.contrib.markup, django.contrib.messages, django.contrib.redirects, django.contrib.sessions, django.contrib.sitemaps, django.contrib.sites, django.contrib.staticfiles, django_crypto, django_extensions, ...]
  8. Django Testing Pain Points • Crowded • Slow • Overbroad • Rough
  9. Django Testing Pain Points • Crowded • Slow • Overbroad • Rough • Extensible but not scalably so
  10. Installation
  11. Installationpip install django-nose
  12. Installationpip install django-noseINSTALLED_APPS = ( ... django_nose, ...)TEST_RUNNER = django_nose.NoseTestSuiteRunner
  13. Installationpip install django-noseINSTALLED_APPS = ( ... django_nose, ...)TEST_RUNNER = django_nose.NoseTestSuiteRunner./manage.py test ...
  14. brevitygoodbye, boilerplate
  15. Discovery
  16. Discoveryproject/frob/tests/__init__.py: from project.frob.tests.test_forms import * from project.frob.tests.test_models import * from project.frob.tests.test_views import * from project.frob.tests.test_tarts import * from project.frob.tests.test_urls import * from project.frob.tests.test_zoos import *
  17. Discovery
  18. Discovery
  19. Discovery(?:^|[b_./-])[Tt]est
  20. Discovery (?:^|[b_./-])[Tt]esttest_something test_things.pytestSomething thing_tests.py
  21. Discovery (?:^|[b_./-])[Tt]esttest_something test_things.pytestSomething thing_tests.py@nottest @istestdef testimonial() def check_boogie()
  22. Discovery (?:^|[b_./-])[Tt]esttest_something test_things.pytestSomething thing_tests.py@nottest @istestdef testimonial() def check_boogie() class SomeCase(TestCase)
  23. Discovery
  24. Discovery• No more accidental shadowing
  25. Discovery• No more accidental shadowing• No more… SocialCampaignAccountComputationTestCase SocialCampaignsItemUserUtilsTestCase SelfServiceCandidateRequestConfirmationTests
  26. Discovery• No more accidental shadowing• No more… SocialCampaignAccountComputationTestCase SocialCampaignsItemUserUtilsTestCase SelfServiceCandidateRequestConfirmationTests• No more forgetting to import
  27. DiscoveryINSTALLED_APPS = [... django.contrib.admin, django.contrib.admindocs, django.contrib.auth, django.contrib.contenttypes, django.contrib.humanize, django.contrib.markup, django.contrib.messages, django.contrib.redirects, django.contrib.sessions, django.contrib.sitemaps, django.contrib.sites, django.contrib.staticfiles, django_crypto, django_extensions, ...]
  28. Discovery
  29. Discovery some_app/tests/__init__.py /test_models.py /test_views.py /test_frobbers.pyanother_app/tests/__init__.py /test_forms.py /test_views.py /sample_data.dat
  30. Discovery some_app/tests/__init__.py /test_models.py /test_views.py /test_frobbers.pyanother_app/tests/__init__.py /test_forms.py /test_views.py /sample_data.dat frob_app/tests/__init__.py /model_tests.py /view_tests.py
  31. Discovery./manage.py test myapp.CacheUtilsTestCase.test_incr
  32. Discovery./manage.py test myapp.CacheUtilsTestCase.test_incr./manage.py test myapp.tests.test_cache:CacheUtilsTestCase.test_incr
  33. Discovery./manage.py test myapp.CacheUtilsTestCase.test_incr./manage.py test myapp.tests.test_cache:CacheUtilsTestCase.test_incr./manage.py test myapp.tests.test_cache
  34. power tools hello, toys
  35. Functions as Tests
  36. Functions as Tests# Look, ma: no class!def test_height_initted(): """We should be able to get a height even on no-tty Terminals.""" t = Terminal(stream=StringIO()) eq_(type(t.height), int)
  37. Functions as Tests# Look, ma: no class!def test_height_initted(): """We should be able to get a height even on no-tty Terminals.""" t = Terminal(stream=StringIO()) eq_(type(t.height), int)
  38. Functions as Tests# Look, ma: no class!def test_height_initted(): """We should be able to get a height even on no-tty Terminals.""" t = Terminal(stream=StringIO()) eq_(type(t.height), int)@with_setup(setup_func, teardown_func)def test_woobie(): ...
  39. Test Generatorsdata = [(thing1, result1), (Steven, Steve), ...]
  40. Test Generatorsdata = [(thing1, result1), (Steven, Steve), ...]def test_munge(): """ Test munging independently on several pieces of data. """ for t, r in data: yield check_datum, t, rdef check_datum(t, r): assert munge(t) == r
  41. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ...
  42. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ..../manage.py test -a selenium
  43. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ..../manage.py test -a selenium./manage.py test -a !selenium
  44. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ..../manage.py test -a selenium./manage.py test -a !selenium@attr(speed=slow)class MyTestCase: def test_long_integration(self): ... def test_end_to_end_something(self): ...
  45. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ..../manage.py test -a selenium./manage.py test -a !selenium@attr(speed=slow)class MyTestCase: def test_long_integration(self): ... def test_end_to_end_something(self): ..../manage.py test speed=slow
  46. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ..../manage.py test -a selenium./manage.py test -a !selenium@attr(speed=slow)class MyTestCase: def test_long_integration(self): ... def test_end_to_end_something(self): ..../manage.py test speed=slow./manage.py test -a selenium,speed=slow # and
  47. Test Attributesfrom nose.plugins.attrib import attr@attr(selenium)def test_click_around(): ..../manage.py test -a selenium./manage.py test -a !selenium@attr(speed=slow)class MyTestCase: def test_long_integration(self): ... def test_end_to_end_something(self): ..../manage.py test speed=slow./manage.py test -a selenium,speed=slow # and./manage.py test -a selenium -a speed=slow # or
  48. XML Output
  49. XML Output./manage.py test --with-xunit
  50. XML Output./manage.py test --with-xunit --xunit-file=funky.xml
  51. XML Output./manage.py test --with-xunit --xunit-file=funky.xml<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="1" errors="1" failures="0" skip="0"> <testcase classname="path_to_test_suite.TestSomething" name="test_it" time="0"> <error type="exceptions.ValueError" message="oops, too many gerbils"> Traceback (most recent call last): ... ValueError: oops, too many gerbils </error> </testcase></testsuite>
  52. Word of Warning./manage.py test
  53. Word of Warning./manage.py test -s
  54. Word of Warning./manage.py test -salias t=./manage.py test --with-progressive -s
  55. More Goodies
  56. More Goodies• Custom error classes
  57. More Goodies• Custom error classes• Extensibility
  58. More Goodies• Custom error classes• Extensibility• Plugins
  59. More Goodies• Custom error classes• Extensibility• Plugins• Parallelization
  60. speed
  61. speeddjango-nose is magic. I lied.
  62. support.mozilla.org
  63. django-nose Speed support.mozilla.org
  64. django-nose Speed support.mozilla.org• 1200 tests
  65. django-nose Speed support.mozilla.org• 1200 tests• 20 minutes to test on Jenkins
  66. django-nose Speed support.mozilla.org• 1200 tests• 20 minutes to test on Jenkins• 5 minutes locally
  67. django-nose Speed
  68. django-nose Speed TESTING!
  69. django-nose Speed• Swordfighting• Switching contexts
  70. django-nose Speed• Swordfighting• Switching contexts• Not running the tests
  71. django-nose Speed• Swordfighting• Switching contexts• Not running the tests• Breaking the build
  72. % time ./manage.py testCreating test database for alias default......blah blah blah./manage.py test 50.75s user 6.01s system 30% cpu 302.594 total
  73. FastFixtureTestCase
  74. FastFixtureTestCase[ { "pk": 1, "model": "forums.forum", "fields": { "name": "Test forum", "slug": "test-forum", "last_post": 25 } }, { "pk": 2, "model": "forums.forum", "fields": { "name": "Another Forum", "slug": "another-forum" } }, { "pk": 3, "model": "forums.forum", "fields": { "name": "Restricted Forum", "slug": "restricted-forum" } }, { "pk": 4, "model": "forums.forum", "fields": { "name": "Visible Restricted Forum", "slug": "visible",
  75. "created": "2010-05-04 13:08:28", FastFixtureTestCase "thread": 5, "author": 47963 } }, { "pk": 26, "model": "forums.post", "fields": { "content": "test", "updated": "2010-05-02 16:08:28", "created": "2010-05-02 16:08:28", "thread": 4, "author": 118533 } }, { "pk": 25, "model": "forums.post", "fields": { "content": "test", "updated": "2010-05-21 16:08:28", "created": "2010-05-21 16:08:28", "thread": 4, "author": 118533 } }, { "pk": 1, "model": "forums.post", "fields": { "content": "This is a test post!", "updated": "2010-04-28 16:06:03", "created": "2010-04-28 16:05:13", "thread": 1, "author": 118533 } }]
  76. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json]
  77. FastFixtureTestCaseSET GLOBAL general_log = ON;
  78. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json]
  79. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  80. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  81. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  82. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  83. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  84. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  85. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  86. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  87. FastFixtureTestCaseclass ForumTestCase(TestCase): fixtures = [users.json, posts.json, forums_permissions.json] def test_new_post_updates_thread(self): ... def test_new_post_updates_forum(self): ... def test_replies_count(self): ... def test_sticky_threads_first(self): ...
  88. FastFixtureTestCaseclass FastFixtureTestCase(TestCase): """Loads fixtures just once per class.""" def setup_class(self): load_fixtures() commit() def run_test(self): run_the_test() rollback() def teardown_class(self): remove_fixtures() commit()
  89. FastFixtureTestCaseclass FastFixtureTestCase(TestCase): """Loads fixtures just once per class.""" def setup_class(self): load_fixtures() commit() def run_test(self): run_the_test() rollback() def teardown_class(self): remove_fixtures() commit()
  90. FastFixtureTestCaseclass FastFixtureTestCase(TestCase): """Loads fixtures just once per class.""" def setup_class(self): load_fixtures() commit() def run_test(self): run_the_test() rollback() def teardown_class(self): remove_fixtures() commit()
  91. FastFixtureTestCaseclass FastFixtureTestCase(TestCase): """Loads fixtures just once per class.""" def setup_class(self): load_fixtures() commit() def run_test(self): run_the_test() rollback() def teardown_class(self): remove_fixtures() commit()
  92. FastFixtureTestCaseclass FastFixtureTestCase(TestCase): """Loads fixtures just once per class.""" """A copy of Django 1.3.0s stock loaddata.py, adapted so that, instead of loading any data, it returns the tables referenced by a set of fixtures so we def setup_class(self): can truncate them (and no others) quickly after were finished with them.""" import os import gzip load_fixtures() import zipfile from django.conf import settings from django.core import serializers commit() from django.db import router, DEFAULT_DB_ALIAS from django.db.models import get_apps from django.utils.itercompat import product try: import bz2 has_bz2 = True except ImportError: has_bz2 = False def run_test(self): def tables_used_by_fixtures(fixture_labels, using=DEFAULT_DB_ALIAS): """Act like Djangos stock loaddata command, but, instead of loading data, return an iterable of the names of the tables into which data would be run_the_test() loaded.""" # Keep a count of the installed objects and fixtures fixture_count = 0 loaded_object_count = 0 rollback() fixture_object_count = 0 tables = set() class SingleZipReader(zipfile.ZipFile): def __init__(self, *args, **kwargs): zipfile.ZipFile.__init__(self, *args, **kwargs) if settings.DEBUG: assert len(self.namelist()) == 1, "Zip-compressed fixtures must contain only one file." def teardown_class(self): def read(self): return zipfile.ZipFile.read(self, self.namelist()[0]) compression_types = { remove_fixtures() None: file, gz: gzip.GzipFile, zip: SingleZipReader } commit() if has_bz2: compression_types[bz2] = bz2.BZ2File app_module_paths = [] for app in get_apps(): if hasattr(app, __path__): # Its a models/ subpackage for path in app.__path__: app_module_paths.append(path) else: # Its a models.py module app_module_paths.append(app.__file__) app_fixtures = [os.path.join(os.path.dirname(path), fixtures) for path in app_module_paths] for fixture_label in fixture_labels: parts = fixture_label.split(.) if len(parts) > 1 and parts[-1] in compression_types:
  93. FastFixtureTestCase
  94. FastFixtureTestCase Queries Stock FixturesPer-Class Fixtures 0 10,000 20,000 30,000 40,000
  95. FastFixtureTestCase Queries Stock Fixtures 37,583Per-Class Fixtures 0 10,000 20,000 30,000 40,000
  96. FastFixtureTestCase Queries Stock Fixtures 37,583Per-Class Fixtures 4,116 0 10,000 20,000 30,000 40,000
  97. FastFixtureTestCase
  98. FastFixtureTestCase Seconds Stock FixturesPer-Class Fixtures 0 100 200 300 400
  99. FastFixtureTestCase Seconds Stock Fixtures 302Per-Class Fixtures 0 100 200 300 400
  100. FastFixtureTestCase Seconds Stock Fixtures 302Per-Class Fixtures 97 0 100 200 300 400
  101. Fixture Bundlingclass ThreadsTemplateTests(FastFixtureTestCase): fixtures = [users.json, posts.json, forums_permissions.json] ...class ForumsTemplateTests(FastFixtureTestCase): fixtures = [users.json, posts.json, forums_permissions.json] ...class NewThreadTests(FastFixtureTestCase): fixtures = [users.json, posts.json, forums_permissions.json] ...
  102. Fixture Bundling fixtures TestCase1→ A B C TestCase2→ A B C D TestCase3→ A B C TestCase4→ B D TestCase5→ A B C D TestCase6→ A B C
  103. Fixture Bundling fixtures TestCase1→ A B C TestCase3→ A B C TestCase6→ A B C TestCase2→ A B C D TestCase5→ A B C D TestCase4→ B D
  104. Fixture Bundling fixtures ☚ load ABC TestCase1→ A B C TestCase3→ A B C TestCase6→ A B C TestCase2→ A B C D TestCase5→ A B C D TestCase4→ B D
  105. Fixture Bundling fixtures ☚ load ABC TestCase1→ A B C TestCase3→ A B C TestCase6→ A B C ☚ load ABCD TestCase2→ A B C D TestCase5→ A B C D TestCase4→ B D
  106. Fixture Bundling fixtures ☚ load ABC TestCase1→ A B C TestCase3→ A B C TestCase6→ A B C ☚ load ABCD TestCase2→ A B C D TestCase5→ A B C D ☚ load BD TestCase4→ B D
  107. Fixture Bundling
  108. Fixture Bundling SecondsPer-Class Fixtures Fixture Bundling 0 25 50 75 100
  109. Fixture Bundling SecondsPer-Class Fixtures 97 Fixture Bundling 0 25 50 75 100
  110. Fixture Bundling SecondsPer-Class Fixtures 97 Fixture Bundling 74 0 25 50 75 100
  111. Fixture Bundling --with-fixture-bundling SecondsPer-Class Fixtures 97 Fixture Bundling 74 0 25 50 75 100
  112. Database Reuse
  113. Database Reuse
  114. Database Reuse SecondsFixture Bundling DB Reuse 0 20 40 60 80
  115. Database Reuse SecondsFixture Bundling 74 DB Reuse 0 20 40 60 80
  116. Database Reuse SecondsFixture Bundling 74 DB Reuse 62 0 20 40 60 80
  117. Database Reuse REUSE_DB=1 ./manage.py test SecondsFixture Bundling 74 DB Reuse 62 0 20 40 60 80
  118. Speed Wrap-up
  119. Speed Wrap-up Seconds Stock DjangoPer-Class Fixtures Fixture Bundling DB Reuse 0 100 200 300 400
  120. Speed Wrap-up Seconds Stock Django 302Per-Class Fixtures Fixture Bundling DB Reuse 0 100 200 300 400
  121. Speed Wrap-up Seconds Stock Django 302Per-Class Fixtures 97 Fixture Bundling DB Reuse 0 100 200 300 400
  122. Speed Wrap-up Seconds Stock Django 302Per-Class Fixtures 97 Fixture Bundling 74 DB Reuse 0 100 200 300 400
  123. Speed Wrap-up Seconds Stock Django 302Per-Class Fixtures 97 Fixture Bundling 74 DB Reuse 62 0 100 200 300 400
  124. TransactionTestCases
  125. TransactionTestCases
  126. Hygienic TransactionTestCases
  127. Hygienic TransactionTestCases class MyNiceTestCase(TransactionTestCase): cleans_up_after_itself = True def _fixture_setup(self): ... # Dont flush.
  128. nose-progressive a decent testing ui
  129. not nose-progressive
  130. not nose-progressive
  131. nose-progressive
  132. nose-progressive
  133. nose-progressive pip install nose-progressive./manage.py test --with-progressive
  134. Ponies to Come• Test models• Coverage• Profiling• Better 1.4 layout support ./manage.py test ! ! --traverse-namespace myproj.myapp
  135. ErikRoseerik@votizen.com
  136. ErikRoseerik@votizen.com django-nose sprint tomorrow, 9am-2pm
  137. ? ErikRose erik@votizen.com django-nose sprint tomorrow, 9am-2pmImage Credits:• Anatomical diagram by Patrick J. Lynch, medical illustrator, edited by user DiebBuche: http://commons.wikimedia.org/wiki/File:Mouth_anatomy-de.svg• “Testing!” comic adapted from http://xkcd.com/303/• Memory hierarchy diagram: http://i.imgur.com/X1Hi1.gif

×