Slide 1

Slide 1 text

smörgåsbord testing of A

Slide 2

Slide 2 text

David Winterbottom @codeinthehole /codeinthehole Impending sausage

Slide 3

Slide 3 text

David Winterbottom @codeinthehole /codeinthehole Grange Hill (only British people who grew up in the 1990s will understand this)

Slide 4

Slide 4 text

Tangent Labs @tangent_labs /tangentlabs See www.tangentlabs.co.uk (PS - we’re hiring!)

Slide 5

Slide 5 text

django-oscar @django_oscar /tangentlabs/django-oscar Oscar is the most popular python e-commerce software on Github. It’s code is so beautiful, it’s been known to make grown men weep. oscarcommerce.com

Slide 6

Slide 6 text

This is Oscar’s test suite - we can see a few things which betray the biases of this talk

Slide 7

Slide 7 text

Nose Nose nose.readthedocs.org ...like nose...

Slide 8

Slide 8 text

Django ...and Django.

Slide 9

Slide 9 text

Dexterity Readability Speed The talk covers 3 topics

Slide 10

Slide 10 text

Who is this? (Hint - it’s not Philip Seymour Hoffman). It’s Kent Beck: creator of jUnit, XP etc

Slide 11

Slide 11 text

“I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence ” ...and he’s a pragmatist. Beware of dogma (100% coverage, one assertion per method etc)

Slide 12

Slide 12 text

Bad tests exist. They can pour concrete over your codebase. It’s ok to delete tests.

Slide 13

Slide 13 text

Dexterity Is there a clever Japanese word for the art of slicing? Dexterity = How to use your test suite effectively

Slide 14

Slide 14 text

Basics Love thy test-runner

Slide 15

Slide 15 text

Run ALL the tests

Slide 16

Slide 16 text

Run tests beneath path Can use your shell’s tab completion for speedy folder selection

Slide 17

Slide 17 text

Run test module

Slide 18

Slide 18 text

Run test class Colons are a pain (if you are trying to write a autocompleter) as they are word delimiters

Slide 19

Slide 19 text

Run test method This is too labour-intensive to type, so we use ...

Slide 20

Slide 20 text

nosecomplete github.com/alonho/nosecomplete Someone solved the colon problem!

Slide 21

Slide 21 text

nosecomplete github.com/alonho/nosecomplete ...and here it is in action.

Slide 22

Slide 22 text

Stories

Slide 23

Slide 23 text

Stories Work on a feature Doing TDD, working from a spec etc

Slide 24

Slide 24 text

Here’s a sample test run

Slide 25

Slide 25 text

https://github.com/asdfasdf/spec Custom test runner You need a custom test-runner

Slide 26

Slide 26 text

https://github.com/asdfasdf/spec Testing one module Run your unit tests as you develop

Slide 27

Slide 27 text

https://github.com/asdfasdf/spec Spec plugin https://github.com/bitprophet/spec Write your test classes and methods as a spec Based on Pinocchio

Slide 28

Slide 28 text

https://github.com/asdfasdf/spec BDD-style output And, write your specs to look like this BDD-style output.

Slide 29

Slide 29 text

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) The Chuckle Brothers are updating their profiles

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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 On the road towards a full BDD framwork like Lettuce/Behave, but you can still use Nose and all its plugins

Slide 32

Slide 32 text

Use-cases Work on a new feature Refactor a feature You’re reviewing a pull request

Slide 33

Slide 33 text

https://github.com/nose attribs/spec Run all the shipping tests!

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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 You can tag slow tests

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

Pattern match tests github.com/iElectric/nose-selecttests Better for lazy developers

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Fail fast

Slide 42

Slide 42 text

Editor-friendly paths github.com/erikrose/nose-progressive Other nice features: prints out failures/errors as they occur, progress bar

Slide 43

Slide 43 text

PDB on failure Need to go up the stack to get to test code

Slide 44

Slide 44 text

Only re-run failed tests Needs to be run twice though

Slide 45

Slide 45 text

Use-cases Work on a new feature Refactor a feature Fix failing tests Commit Before pushing etc

Slide 46

Slide 46 text

Run all the tests as fast as possible

Slide 47

Slide 47 text

No plugins

Slide 48

Slide 48 text

Parallelise if you use tox, then try detox

Slide 49

Slide 49 text

Speed Watch Gary Bernhardt’s talks

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

0 4 8 12 16 20 24 Focus / flow / “zone” Test suite time (seconds) Pre-commit hook? Everything’s rosy when your test suite runs in 100ms

Slide 52

Slide 52 text

0 4 8 12 16 20 24 Focus / flow / “zone” Test suite time (seconds) But watch out for the threshold of doom

Slide 53

Slide 53 text

0 4 8 12 16 20 24 Test suite time (seconds) Oh no - you’re reading Hacker News!

Slide 54

Slide 54 text

Tips for speeding up tests will be in another talk Oscar distinguishes between unit, integration and functional (acceptance) tests to try and steer people towards unit tests

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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! All my favourite bits of Python in one curio

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

http://bit.ly/156c48x Number of tests Seconds Oscar’s test suite has doubled number of tests while halving the time to run.

Slide 61

Slide 61 text

PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] Using this hasher makes a big difference.

Slide 62

Slide 62 text

--processes=8 Maik spotted this option for Nose and we all had a cup of tea to celebrate.

Slide 63

Slide 63 text

http://bit.ly/2gZnhj Boy scout rule... Always leave the campsite cleaner than how you found it

Slide 64

Slide 64 text

Readability

Slide 65

Slide 65 text

http://bit.ly/19ganRJ (Refactoring a Test) Book has a great chapter on refactoring a test from dirty to clean

Slide 66

Slide 66 text

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) This is a bad test

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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 Numbered variable names make baby jesus cry

Slide 72

Slide 72 text

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) This is the same test, but cleaned up

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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 Tests should only show relevant details

Slide 75

Slide 75 text

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 Dumb example but a good thing to do in general

Slide 76

Slide 76 text

PyHamcrest github.com/hamcrest/PyHamcrest >>> from hamcrest import * >>> assert_that( shipping.charge, is_(equal_to(D('4.99')))) >>> assert_that( "RESTful".lower(), contains_string("stfu")) Hamcrest - more readable but too many parentheses?

Slide 77

Slide 77 text

should-dsl github.com/nsi-iff/should-dsl >>> from should_dsl import should, should_not >>> {} |should| be_empty >>> user.email |should| contain("@") >>> basket |should| contain(product) Crazy voodoo here, but quite readable ultimately

Slide 78

Slide 78 text

This was the best image of a Smorgasbord I could find

Slide 79

Slide 79 text

wiki.python.org/moin/PythonTestingToolsTaxonomy For all your python testing needs.