Dial T for Testing

Dial T for Testing

A talk about testing and Django from the March 2014 DJUGL meetup.

52d39c7b27386ca98bc016119d95b8b8?s=128

David Winterbottom

March 12, 2014
Tweet

Transcript

  1. T testing for Dial

  2. David Winterbottom @codeinthehole /codeinthehole

  3. Tangent Labs @tangent_labs /tangentlabs

  4. django-oscar @django_oscar /tangentlabs/django-oscar

  5. None
  6. Nose Nose nose.readthedocs.org

  7. Django

  8. Dexterity

  9. Basics

  10. Run ALL the tests

  11. Run tests beneath path

  12. Run test module

  13. Run test class

  14. Run test method

  15. nosecomplete github.com/alonho/nosecomplete

  16. nosecomplete github.com/alonho/nosecomplete

  17. Stories

  18. Stories Work on a feature

  19. None
  20. https://github.com/asdfasdf/spec Custom test runner

  21. https://github.com/asdfasdf/spec Testing one module

  22. https://github.com/asdfasdf/spec Spec plugin https://github.com/bitprophet/spec

  23. https://github.com/asdfasdf/spec BDD-style output

  24. class TestASignedInUser(WebTestCase):! ! def setUp(self):! self.user = G(User)! self.order =

    factories.create_order(user=self.user)! ! def test_can_see_their_email_address_on_the_profile_page(self):! profile_page = self.app.get(! reverse('customer:summary'), user=self.user)! self.assertTrue(self.email in profile_page.content)! ! def test_can_update_their_name(self):! profile_form_page = self.app.get(! reverse('customer:profile-update'), user=self.user)! form = profile_form_page.forms['profile_form']! form['first_name'] = 'Barry'! form['last_name'] = 'Chuckle'! response = form.submit()! self.assertRedirects(response, reverse('customer:summary'))! ! # Reload user! user = User.objects.get(id=self.user.id)! self.assertEquals("Barry", user.first_name)! self.assertEquals("Chuckle", user.last_name)!
  25. class TestASignedInUser(WebTestCase):! ! def setUp(self):! self.user = G(User)! self.order =

    factoies.create_order(user=self.user)! ! def test_can_see_their_email_address_on_the_profile_page(self):! profile_page = self.app.get(! reverse('customer:summary'), user=self.user)! self.assertTrue(self.email in profile_page.content)! ! def test_can_update_their_name(self):! profile_form_page = self.app.get(! reverse('customer:profile-update'), user=self.user)! form = profile_form_page.forms['profile_form']! form['first_name'] = 'Barry'! form['last_name'] = 'Chuckle'! response = form.submit()! self.assertRedirects(response, reverse('customer:summary'))! ! # Reload user! user = User.objects.get(id=self.user.id)! self.assertEquals("Barry", user.first_name)! self.assertEquals("Chuckle", user.last_name)! System under test
  26. class TestASignedInUser(WebTestCase):! ! def setUp(self):! self.user = G(User)! self.order =

    factories.create_order(user=self.user)! ! def test_can_see_their_email_address_on_the_profile_page(self):! profile_page = self.app.get(! reverse('customer:summary'), user=self.user)! self.assertTrue(self.email in profile_page.content)! ! def test_can_update_their_name(self):! profile_form_page = self.app.get(! reverse('customer:profile-update'), user=self.user)! form = profile_form_page.forms['profile_form']! form['first_name'] = 'Barry'! form['last_name'] = 'Chuckle'! response = form.submit()! self.assertRedirects(response, reverse('customer:summary'))! ! # Reload user! user = User.objects.get(id=self.user.id)! self.assertEquals("Barry", user.first_name)! self.assertEquals("Chuckle", user.last_name)! Specification
  27. Use-cases Work on a new feature Refactor a feature

  28. https://github.com/nose attribs/spec

  29. nose.readthedocs.org/en/latest/plugins/attrib.html Run tagged tests

  30. from nose.plugins.attrib import attr! ! ! @attr('shipping')! class TestFreeShippping(TestCase):! !

    def test_is_free_for_empty_basket(self):! ...! ! ! class TestFixedPriceShipping(TestCase):! ! @attr(slow=True)! def test_returns_fixed_price_for_basket(self):! ...!
  31. from nose.plugins.attrib import attr! ! ! @attr('shipping')! class TestFreeShippping(TestCase):! !

    def test_is_free_for_empty_basket(self):! ...! ! ! class TestFixedPriceShipping(TestCase):! ! @attr(slow=True)! def test_returns_fixed_price_for_basket(self):! ...! Wrap a class
  32. from nose.plugins.attrib import attr! ! ! @attr('shipping')! class TestFreeShippping(TestCase):! !

    def test_is_free_for_empty_basket(self):! ...! ! ! class TestFixedPriceShipping(TestCase):! ! @attr(slow=True)! def test_returns_fixed_price_for_basket(self):! ...! Wrap a method
  33. None
  34. Pattern match tests github.com/iElectric/nose-selecttests

  35. Use-cases Work on a new feature Refactor a feature Fix

    failing tests
  36. Fail fast

  37. Editor-friendly paths github.com/erikrose/nose-progressive

  38. PDB on failure

  39. Only re-run failed tests

  40. Use-cases Work on a new feature Refactor a feature Fix

    failing tests Ready to push?
  41. None
  42. No plugins

  43. Parallelise

  44. Speed

  45. 0 4 8 12 16 20 24 Focus / flow

    / “zone” Test suite time (seconds)
  46. 0 4 8 12 16 20 24 Focus / flow

    / “zone” Test suite time (seconds) Pre-commit hook?
  47. 0 4 8 12 16 20 24 Focus / flow

    / “zone” Test suite time (seconds)
  48. 0 4 8 12 16 20 24 Test suite time

    (seconds)
  49. None
  50. None
  51. Scam? blog.thecodewhisperer.com/2010/10/16/integrated-tests-are-a-scam/

  52. import mock! from django.utils.functional import curry! ! no_database = curry(!

    mock.patch, 'django.db.backends.util.CursorWrapper',! Mock(side_effect=RuntimeError(! "Using the database is not permitted")))! ! ! @no_database()! class AUnitTest(TestCase):! ...! http://bit.ly/126vPrt
  53. import mock! from django.utils.functional import curry! ! no_database = curry(!

    mock.patch, 'django.db.backends.util.CursorWrapper',! Mock(side_effect=RuntimeError(! "Using the database is not permitted")))! ! ! @no_database()! class AUnitTest(TestCase):! ...! http://bit.ly/126vPrt Currying mock.patch!
  54. import mock! from django.utils.functional import curry! ! no_database = curry(!

    mock.patch, 'django.db.backends.util.CursorWrapper',! Mock(side_effect=RuntimeError(! "Using the database is not permitted")))! ! ! @no_database()! class AUnitTest(TestCase):! ...! http://bit.ly/126vPrt Wrap classes/methods
  55. http://bit.ly/156c48x Number of tests Seconds

  56. PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']

  57. --processes=8

  58. http://bit.ly/2gZnhj Boy scout rule...

  59. Readability

  60. http://bit.ly/19ganRJ (Refactoring a Test)

  61. class TestBasket(TestCase):! ! def test_add_items(self):! # Create products! product_class =

    ProductClass(! name="Books", require_shipping=True)! product1 = Product.objects.create(title="My first book")! product2 = Product.objects.create(title="My second book")! partner = Partner.objects.create(name="Book Partner")! stockrecord1 = StockRecord(! price_excl_tax=D('12.00'), partner=partner)! stockrecord2 = StockRecord(! price_excl_tax=D('14.00'), partner=partner)! ! # Add to basket! basket = Basket()! basket.add(product1)! basket.add(product2)! ! total = basket.total_excl_tax! ! self.assertEqual(D('26.00'), total)! self.assertEqual(basket.num_lines, 2)! self.assertFalse(basket.is_empty)!
  62. class TestBasket(TestCase):! ! def test_add_items(self):! # Create products! product_class =

    ProductClass(! name="Books", require_shipping=True)! product1 = Product.objects.create(title="My first book")! product2 = Product.objects.create(title="My second book")! partner = Partner.objects.create(name="Book Partner")! stockrecord1 = StockRecord(! price_excl_tax=D('12.00'), partner=partner)! stockrecord2 = StockRecord(! price_excl_tax=D('14.00'), partner=partner)! ! # Add to basket! basket = Basket()! basket.add(product1)! basket.add(product2)! ! total = basket.total_excl_tax! ! self.assertEqual(D('26.00'), total)! self.assertEqual(basket.num_lines, 2)! self.assertFalse(basket.is_empty)! Bad naming
  63. class TestBasket(TestCase):! ! def test_add_items(self):! # Create products! product_class =

    ProductClass(! name="Books", require_shipping=True)! product1 = Product.objects.create(title="My first book")! product2 = Product.objects.create(title="My second book")! partner = Partner.objects.create(name="Book Partner")! stockrecord1 = StockRecord(! price_excl_tax=D('12.00'), partner=partner)! stockrecord2 = StockRecord(! price_excl_tax=D('14.00'), partner=partner)! ! # Add to basket! basket = Basket()! basket.add(product1)! basket.add(product2)! ! total = basket.total_excl_tax! ! self.assertEqual(D('26.00'), total)! self.assertEqual(basket.num_lines, 2)! self.assertFalse(basket.is_empty)! Heavy set-up
  64. class TestBasket(TestCase):! ! def test_add_items(self):! # Create products! product_class =

    ProductClass(! name="Books", require_shipping=True)! product1 = Product.objects.create(title="My first book")! product2 = Product.objects.create(title="My second book")! partner = Partner.objects.create(name="Book Partner")! stockrecord1 = StockRecord(! price_excl_tax=D('12.00'), partner=partner)! stockrecord2 = StockRecord(! price_excl_tax=D('14.00'), partner=partner)! ! # Add to basket! basket = Basket()! basket.add(product1)! basket.add(product2)! ! total = basket.total_excl_tax! ! self.assertEqual(D('26.00'), total)! self.assertEqual(basket.num_lines, 2)! self.assertFalse(basket.is_empty)! Noise
  65. class TestBasket(TestCase):! ! def test_add_items(self):! # Create products! product_class =

    ProductClass(! name="Books", require_shipping=True)! product1 = Product.objects.create(title="My first book")! product2 = Product.objects.create(title="My second book")! partner = Partner.objects.create(name="Book Partner")! stockrecord1 = StockRecord(! price_excl_tax=D('12.00'), partner=partner)! stockrecord2 = StockRecord(! price_excl_tax=D('14.00'), partner=partner)! ! # Add to basket! basket = Basket()! basket.add(product1)! basket.add(product2)! ! total = basket.total_excl_tax! ! self.assertEqual(D('26.00'), total)! self.assertEqual(basket.num_lines, 2)! self.assertFalse(basket.is_empty)! Unclear assertions
  66. class TestBasket(TestCase):! ! def test_add_items(self):! # Create products! product_class =

    ProductClass(! name="Books", require_shipping=True)! product1 = Product.objects.create(title="My first book")! product2 = Product.objects.create(title="My second book")! partner = Partner.objects.create(name="Book Partner")! stockrecord1 = StockRecord(! price_excl_tax=D('12.00'), partner=partner)! stockrecord2 = StockRecord(! price_excl_tax=D('14.00'), partner=partner)! ! # Add to basket! basket = Basket()! basket.add(product1)! basket.add(product2)! ! total = basket.total_excl_tax! ! self.assertEqual(D('26.00'), total)! self.assertEqual(basket.num_lines, 2)! self.assertFalse(basket.is_empty)! Sloppy coding
  67. class TestBasketTotal(TestCase):! ! def setUp(self):! self.basket = Basket()! ! def

    test_is_correct_after_adding_multiple_items(self):! products = [! factory.create_product(price_excl_tax=D('12.00')),! factory.create_product(price_excl_tax=D('14.00')),! ]! for product in products:! self.basket.add_product(product)! self.assertEqual(! D('12.00') + D('14.00'), self.basket.total_excl_tax)!
  68. class TestBasketTotal(TestCase):! ! def setUp(self):! self.basket = Basket()! ! def

    test_is_correct_after_adding_multiple_items(self):! products = [! factory.create_product(price_excl_tax=D('12.00')),! factory.create_product(price_excl_tax=D('14.00')),! ]! for product in products:! self.basket.add_product(product)! self.assertEqual(! D('12.00') + D('14.00'), self.basket.total_excl_tax)! Descriptive names
  69. class TestBasketTotal(TestCase):! ! def setUp(self):! self.basket = Basket()! ! def

    test_is_correct_after_adding_multiple_items(self):! products = [! factory.create_product(price_excl_tax=D('12.00')),! factory.create_product(price_excl_tax=D('14.00')),! ]! for product in products:! self.basket.add_product(product)! self.assertEqual(! D('12.00') + D('14.00'), self.basket.total_excl_tax)! Factory functions
  70. class TestBasketTotal(TestCase):! ! def setUp(self):! self.basket = Basket()! ! def

    test_is_correct_after_adding_multiple_items(self):! products = [! factory.create_product(price_excl_tax=D('12.00')),! factory.create_product(price_excl_tax=D('14.00')),! ]! for product in products:! self.basket.add_product(product)! self.assertEqual(! D('12.00') + D('14.00'), self.basket.total_excl_tax)! Intention revealing
  71. WebTest webtest.readthedocs.org/ def test_contact_us(self):! home = self.app.get('/')! ! # Fill

    in form! contact_us = home.click(linkid="contact_us_link")! form = contact_us.forms['profile_form']! form['name'] = 'Barry Chuckle'! form['query'] = 'To me?'! confirm = form.submit().follow()! ! self.assertTrue('Thanks Barry' in confirm.content)!
  72. None
  73. red green refactor

  74. failing test passing test clean test fast test

  75. no tests tests readable tests fast, readable tests

  76. •Test suite is there to help you •Make it clear

    and easy to read •Make it fast •Master your test runner TLDR
  77. •Write tests using the spec style •Use WebTest (not the

    Django client) Homework