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

Dial T for Testing

Dial T for Testing

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

David Winterbottom

March 12, 2014
Tweet

More Decks by David Winterbottom

Other Decks in Programming

Transcript

  1. T
    testing
    for
    Dial

    View Slide

  2. David Winterbottom
    @codeinthehole
    /codeinthehole

    View Slide

  3. Tangent Labs
    @tangent_labs
    /tangentlabs

    View Slide

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

    View Slide

  5. View Slide

  6. Nose
    Nose
    nose.readthedocs.org

    View Slide

  7. Django

    View Slide

  8. Dexterity

    View Slide

  9. Basics

    View Slide

  10. Run ALL the tests

    View Slide

  11. Run tests beneath path

    View Slide

  12. Run test module

    View Slide

  13. Run test class

    View Slide

  14. Run test method

    View Slide

  15. nosecomplete
    github.com/alonho/nosecomplete

    View Slide

  16. nosecomplete
    github.com/alonho/nosecomplete

    View Slide

  17. Stories

    View Slide

  18. Stories
    Work on a feature

    View Slide

  19. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)!

    View Slide

  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

    View Slide

  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

    View Slide

  27. Use-cases
    Work on a new feature
    Refactor a feature

    View Slide

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

    View Slide

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

    View Slide

  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):!
    ...!

    View Slide

  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

    View Slide

  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

    View Slide

  33. View Slide

  34. Pattern match tests
    github.com/iElectric/nose-selecttests

    View Slide

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

    View Slide

  36. Fail fast

    View Slide

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

    View Slide

  38. PDB on failure

    View Slide

  39. Only re-run failed tests

    View Slide

  40. Use-cases
    Work on a new feature
    Refactor a feature
    Fix failing tests
    Ready to push?

    View Slide

  41. View Slide

  42. No plugins

    View Slide

  43. Parallelise

    View Slide

  44. Speed

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. 0 4 8 12 16 20 24
    Test suite time (seconds)

    View Slide

  49. View Slide

  50. View Slide

  51. Scam?
    blog.thecodewhisperer.com/2010/10/16/integrated-tests-are-a-scam/

    View Slide

  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

    View Slide

  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!

    View Slide

  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

    View Slide

  55. http://bit.ly/156c48x
    Number of tests
    Seconds

    View Slide

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

    View Slide

  57. --processes=8

    View Slide

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

    View Slide

  59. Readability

    View Slide

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

    View Slide

  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)!

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)!

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)!

    View Slide

  72. View Slide

  73. red
    green
    refactor

    View Slide

  74. failing test
    passing test
    clean test
    fast test

    View Slide

  75. no tests
    tests
    readable tests
    fast, readable tests

    View Slide

  76. •Test suite is there to help you
    •Make it clear and easy to read
    •Make it fast
    •Master your test runner
    TLDR

    View Slide

  77. •Write tests using the spec style
    •Use WebTest (not the Django
    client)
    Homework

    View Slide