Django and the testing pyramid - DjangoCon Europe 2017

Django and the testing pyramid - DjangoCon Europe 2017

Video: https://www.youtube.com/watch?v=Mb4P0vnmymM

Model mommy lettuce factory boy expects gherkin behave. Travis mock splinter nose phantom hypothesis!

Most developers recognise the importance of testing in building maintainable software, but the sheer volume of tools and approaches can be bewildering and sometimes make it sound like you’re talking a different language.

In this talk we’ll look at the testing pyramid. We’ll discuss how approaching our testing in layers can increase developer productivity by reducing the time taken to run our test suites. We’ll look at the importance of different testing types including integration and acceptance testing. Finally we’ll introduce some tools and packages you can use to make writing good tests easier.

By the end you'll be able to talk TDD and BDD like a native!

309287088ccfe196428a5dbe2b051c48?s=128

Aaron Bassett

April 04, 2017
Tweet

Transcript

  1. Django and the testing pyramid @aaronbassett

  2. Energy Pyramid

  3. Food Pyramid

  4. None
  5. functional integration unit The testing pyramid

  6. Unit Tests test *1* thing in isolation

  7. SUPER FAST

  8. Integration Tests test things work together

  9. Functional Tests end-to-end testing

  10. Manual Testing people cycles not processor cycles

  11. Feature: Number verification Users can add verified mobile numbers to

    their profile Scenario: Adding a valid phone number Given I'm an authenticated user And I have a valid phone number When I go to the phone number page And I press the verify button Then I should not see the error message
  12. Feature: Number verification Users can add verified mobile numbers to

    their profile Scenario: Adding a valid phone number Given I'm an authenticated user And I have a valid phone number When I go to the phone number page And I press the verify button Then I should not see the error message
  13. Feature: Number verification Users can add verified mobile numbers to

    their profile Scenario: User enters an invalid phone number Given I'm an authenticated user When I go to the phone number page And I enter ‘not a number’ And I press the verify button Then I should see the error message
  14. Feature: Number verification Users can add verified mobile numbers to

    their profile Scenario: User enters an invalid phone number Given I'm an authenticated user When I go to the phone number page And I enter <invalid_number> And I press the verify button Then I should see the error message Examples: | invalid_number | | foo@example.com | | 0 | | +441411111111 | | +44712345678 |
  15. Feature: Number verification Users can add verified mobile numbers to

    their profile Scenario: User enters an invalid phone number Given I'm an authenticated user When I go to the phone number page And I enter <invalid_number> And I press the verify button Then I should see the error message Examples: | invalid_number | | foo@example.com | | 0 | | +441411111111 | | +44712345678 |
  16. functional integration unit

  17. @given("I'm an authenticated user") def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user')

    browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  18. @given("I'm an authenticated user") def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user')

    browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  19. @given("I'm an authenticated user") def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user')

    browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  20. @given("I'm an authenticated user") def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user')

    browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  21. splinter & pytest-splinter

  22. None
  23. None
  24. @when('I go to the phone number page') def go_to_number_submission_page(browser): browser.visit(urljoin(browser.url,

    '/number/') @when('I enter <invalid_number>') def enter_invalid_number(browser, invalid_number): browser.fill('number', invalid_number) @when('I press the verify button') def submit_number(browser): browser.find_by_css('button[name=verify]').first.click()
  25. @when('I go to the phone number page') def go_to_number_submission_page(browser): browser.visit(urljoin(browser.url,

    '/number/') @when('I enter <invalid_number>') def enter_invalid_number(browser, invalid_number): browser.fill('number', invalid_number) @when('I press the verify button') def submit_number(browser): browser.find_by_css('button[name=verify]').first.click()
  26. @when('I go to the phone number page') def go_to_number_submission_page(browser): browser.visit(urljoin(browser.url,

    '/number/') @when('I enter <invalid_number>') def enter_invalid_number(browser, invalid_number): browser.fill('number', invalid_number) @when('I press the verify button') def submit_number(browser): browser.find_by_css('button[name=verify]').first.click()
  27. @then('I should see the error message') def has_error_message(browser): browser.find_by_css('.message.error').first

  28. @then('I should see the error message') def has_error_message(browser): browser.find_by_css('.message.error').first

  29. @then('I should see the error message') def has_error_message(browser): browser.find_by_css('.message.error').first

  30. @given("I'm an authenticated user") def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user')

    browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  31. @given('I\'m logged in') @given("I'm an authenticated user") @given('I log in')

    def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user') browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  32. @given('I\'m logged in') @given("I'm an authenticated user") @given('I log in')

    def authenticat_user(browser): browser.visit(urljoin(browser.url, '/login/') browser.fill('username', 'test_user') browser.fill('password', 'test_password') button = browser.find_by_css('button[name=submit]').first.click()
  33. functional integration unit

  34. def start_number_verification(request): if request.method == "POST": if request.user.is_authenticated: number =

    request.POST.get("number", None) if re.search("[^0-9+-\\s]+", number): raise Exception existing_validation_requests = ValidationRequest.objects.filter( number=number, active=True ) if existing_validation_requests.exists(): raise Exception # AND SO ON...
  35. None
  36. None
  37. ryannevius.com

  38. functional integration unit

  39. def start_number_verification(request): if request.method == "POST": if request.user.is_authenticated: number =

    request.POST.get("number", None) if re.search("[^0-9+-\\s]+", number): raise Exception existing_validation_requests = ValidationRequest.objects.filter( number=number, active=True ) if existing_validation_requests.exists(): raise Exception # AND SO ON...
  40. def start_number_verification(request): if request.method == "POST": if request.user.is_authenticated: number =

    request.POST.get("number", None) if re.search("[^0-9+-\\s]+", number): raise Exception existing_validation_requests = ValidationRequest.objects.filter( number=number, active=True ) if existing_validation_requests.exists(): raise Exception # AND SO ON...
  41. mock.patch

  42. Connected

  43. None
  44. Modular

  45. ⌥⌘M

  46. class NumberVerificationView(View): def start_number_verification(request): if request.user.is_authenticated: number = request.POST.get("number", None)

    if re.search("[^0-9+-\\s]+", number): raise Exception existing_validation_requests = ValidationRequest.objects.filter( number=number, active=True ) if existing_validation_requests.exists(): raise Exception # AND SO ON...
  47. class NumberVerificationView(LoginRequiredMixin, View): login_url = '/login/' redirect_field_name = 'redirect_to' def

    start_number_verification(request): number = request.POST.get("number", None) if re.search("[^0-9+-\\s]+", number): raise Exception existing_validation_requests = ValidationRequest.objects.filter( number=number, active=True ) if existing_validation_requests.exists(): raise Exception # AND SO ON...
  48. class NumberVerificationView(LoginRequiredMixin, FormView): ... def form_valid(self, form): number = request.POST.get("number",

    None) if re.search("[^0-9+-\\s]+", number): raise Exception existing_validation_requests = ValidationRequest.objects.filter( number=number, active=True ) if existing_validation_requests.exists(): raise Exception # AND SO ON...
  49. class NumberVerificationView(LoginRequiredMixin, FormView): ... def form_valid(self, form): # AND SO

    ON... return super(ContactView, self).form_valid(form)
  50. def validate_phone_number_characters(value): if re.search("[^0-9+-\\s]+", value): raise ValidationError( _('%(value) contains invalid

    characters'), params={'value': value}, )
  51. def validate_no_active_verification_requests(value): existing_validation_requests = ValidationRequest.objects.filter( number=value, active=True ) if existing_validation_requests.exists():

    raise ValidationError( _('There is already a pending request for %(value)'), params={'value': value}, )
  52. SUPER FAST

  53. property based testing http://hypothesis.works/

  54. @given(invalid_phone_number) @settings(max_examples=1000) def test_names_match_our_requirements(number): with pytest.raises(ValidationError, message="Expecting ValidationError"): validate_phone_number_characters(number)

  55. @given(invalid_phone_number) @settings(max_examples=1000) def test_names_match_our_requirements(number): with pytest.raises(ValidationError, message="Expecting ValidationError"): validate_phone_number_characters(number)

  56. None
  57. None
  58. httmock

  59. functional integration unit

  60. None
  61. @pytest.mark.slowtest def test_function(): pass

  62. @pytest.mark.slowtest def test_function(): pass

  63. None
  64. None
  65. None
  66. None
  67. None
  68. STOP!

  69. functional integration unit

  70. functional integration unit

  71. functional integration unit

  72. Grazie

  73. Django and the testing pyramid @aaronbassett