Django and the testing pyramid @aaronbassett

rgy id Ene am r Py

d id Foo am r Py

The testing pyramid functional integration unit

Unit Tests test 1 thing in isolation

SUPER FAST

Integration Tests test things work together

Functional Tests end-to-end testing

Manual Testing people cycles not processor cycles

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

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

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

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

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

functional integration unit

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

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

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

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

splinter & pytest-splinter

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

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

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

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

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

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

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

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

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

functional integration unit

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

ryannevius.com

functional integration unit

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

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

mock.patch

Connected

Modular

⌥⌘M

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

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

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

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

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

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

SUPER FAST

property based testing http://hypothesis.works/

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

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

httmock

functional integration unit

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

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

STOP!

functional integration unit

functional integration unit

functional integration unit

Grazie

Django and the testing pyramid @aaronbassett