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

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!

Aaron Bassett

April 04, 2017
Tweet

More Decks by Aaron Bassett

Other Decks in Programming

Transcript

  1. Django and the
    testing pyramid
    @aaronbassett

    View Slide

  2. Energy
    Pyramid

    View Slide

  3. Food
    Pyramid

    View Slide

  4. View Slide

  5. functional
    integration
    unit
    The testing
    pyramid

    View Slide

  6. Unit Tests
    test *1* thing in isolation

    View Slide

  7. SUPER
    FAST

    View Slide

  8. Integration Tests
    test things work together

    View Slide

  9. Functional Tests
    end-to-end testing

    View Slide

  10. Manual Testing
    people cycles not processor cycles

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    And I press the verify button
    Then I should see the error message
    Examples:
    | invalid_number |
    | [email protected] |
    | 0 |
    | +441411111111 |
    | +44712345678 |

    View Slide

  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
    And I press the verify button
    Then I should see the error message
    Examples:
    | invalid_number |
    | [email protected] |
    | 0 |
    | +441411111111 |
    | +44712345678 |

    View Slide

  16. functional
    integration
    unit

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. splinter &
    pytest-splinter

    View Slide

  22. View Slide

  23. View Slide

  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 ')
    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()

    View Slide

  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 ')
    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()

    View Slide

  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 ')
    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()

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. functional
    integration
    unit

    View Slide

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

    View Slide

  35. View Slide

  36. View Slide

  37. ryannevius.com

    View Slide

  38. functional
    integration
    unit

    View Slide

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

    View Slide

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

    View Slide

  41. mock.patch

    View Slide

  42. Connected

    View Slide

  43. View Slide

  44. Modular

    View Slide

  45. ⌥⌘M

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  49. class NumberVerificationView(LoginRequiredMixin, FormView):
    ...
    def form_valid(self, form):
    # AND SO ON...
    return super(ContactView, self).form_valid(form)

    View Slide

  50. def validate_phone_number_characters(value):
    if re.search("[^0-9+-\\s]+", value):
    raise ValidationError(
    _('%(value) contains invalid characters'),
    params={'value': value},
    )

    View Slide

  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},
    )

    View Slide

  52. SUPER
    FAST

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  56. View Slide

  57. View Slide

  58. httmock

    View Slide

  59. functional
    integration
    unit

    View Slide

  60. View Slide

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

    View Slide

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

    View Slide

  63. View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. STOP!

    View Slide

  69. functional
    integration
    unit

    View Slide

  70. functional
    integration
    unit

    View Slide

  71. functional
    integration
    unit

    View Slide

  72. Grazie

    View Slide

  73. Django and the
    testing pyramid
    @aaronbassett

    View Slide