Django and the testing pyramid

A presentation at DjangoCon Europe 2017 in April 2017 in Florence, Metropolitan City of Florence, Italy by Aaron Bassett

Slide 1

Slide 1

Django and the testing pyramid @aaronbassett

Slide 2

Slide 2

rgy id Ene am r Py

Slide 3

Slide 3

d id Foo am r Py

Slide 4

Slide 4

Slide 5

Slide 5

The testing pyramid functional integration unit

Slide 6

Slide 6

Unit Tests test 1 thing in isolation

Slide 7

Slide 7

SUPER FAST

Slide 8

Slide 8

Integration Tests test things work together

Slide 9

Slide 9

Functional Tests end-to-end testing

Slide 10

Slide 10

Manual Testing people cycles not processor cycles

Slide 11

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

Slide 12

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

Slide 13

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

Slide 14

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 <invalid_number> And I press the verify button Then I should see the error message Examples: | invalid_number | foo@example.com | 0 | +441411111111 | +44712345678 | | | | |

Slide 15

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 <invalid_number> And I press the verify button Then I should see the error message Examples: | invalid_number | foo@example.com | 0 | +441411111111 | +44712345678 | | | | |

Slide 16

Slide 16

functional integration unit

Slide 17

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

Slide 18

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

Slide 19

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

Slide 20

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

Slide 21

Slide 21

splinter & pytest-splinter

Slide 22

Slide 22

Slide 23

Slide 23

Slide 24

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

Slide 25

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

Slide 26

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

Slide 27

Slide 27

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

Slide 28

Slide 28

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

Slide 29

Slide 29

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

Slide 30

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

Slide 31

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

Slide 32

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

Slide 33

Slide 33

functional integration unit

Slide 34

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

Slide 35

Slide 35

Slide 36

Slide 36

Slide 37

Slide 37

ryannevius.com

Slide 38

Slide 38

functional integration unit

Slide 39

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

Slide 40

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

Slide 41

Slide 41

mock.patch

Slide 42

Slide 42

Connected

Slide 43

Slide 43

Slide 44

Slide 44

Modular

Slide 45

Slide 45

⌥⌘M

Slide 46

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

Slide 47

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

Slide 48

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

Slide 49

Slide 49

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

Slide 50

Slide 50

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

Slide 51

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

Slide 52

Slide 52

SUPER FAST

Slide 53

Slide 53

property based testing http://hypothesis.works/

Slide 54

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)

Slide 55

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)

Slide 56

Slide 56

Slide 57

Slide 57

Slide 58

Slide 58

httmock

Slide 59

Slide 59

functional integration unit

Slide 60

Slide 60

Slide 61

Slide 61

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

Slide 62

Slide 62

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

Slide 63

Slide 63

Slide 64

Slide 64

Slide 65

Slide 65

Slide 66

Slide 66

Slide 67

Slide 67

Slide 68

Slide 68

STOP!

Slide 69

Slide 69

functional integration unit

Slide 70

Slide 70

functional integration unit

Slide 71

Slide 71

functional integration unit

Slide 72

Slide 72

Grazie

Slide 73

Slide 73

Django and the testing pyramid @aaronbassett