A presentation at DjangoCon Europe 2017 in in Florence, Metropolitan City of Florence, Italy by Aaron Bassett
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
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!
Here’s what was said about this presentation on social media.
Always enjoy seeing @aaronbassett speak. Today it’s on the testing pyramid, and behaviour driven development pic.twitter.com/omPnZXGauT
— Mark Steadman (@iamsteadman) April 4, 2017
Day 2 of @DjangoConEurope. Ready 4 more great talks like @limedaring @m_holtermann @mrchrisadams @aaronbassett @freakboy3742 pic.twitter.com/tHNPMA6fA5
— Spencer Winegar (@utahpadre) April 4, 2017
🎥 Time for @aaronbassett talk at #DjangoCon, Django and the testing pyramid. Enjoy! 🎥 pic.twitter.com/1cEA61iDH2
— DjangoCon Europe (@DjangoConEurope) April 4, 2017
Top is of pyramid is manual testing not the Illuminati @aaronbassett on testing at #djangocon pic.twitter.com/yAuuHknlld
— daniel rios (@misterrios) April 4, 2017
"Django and the testing pyramid" @aaronbassett at @DjangoConEurope #django #bdd #gherkin #tdd #unit #intregation #functional #testing pic.twitter.com/V42Pmxn5aV
— Paolo Melchiorre (@pauloxnet) April 4, 2017