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

How to Test your Web Applications

How to Test your Web Applications

Writing tests is not that easy. People tend to overlook this task, often seen as less interesting than writing “real code”. Until they join a new company, where nobody told them they would have to maintain a legacy codebase, with temporary fixes everywhere and a test coverage of 30%, and that developers who wrote it already left several years ago…

In this talk, we will see how to write tests with Pytest for your web applications: from acceptance tests, to unit tests, without forgetting integration tests of course! Applying best practices like Behavior-Driven Development, we will try to identify traps on our way and learn how to avoid them. And because we are living in the 21st century, we will also automate our development workflow with Docker Compose, to make our day-to-day work more enjoyable.

[Talk given at PyConWeb 2018]

Alexandre Figura

June 30, 2018
Tweet

More Decks by Alexandre Figura

Other Decks in Programming

Transcript

  1. Who Am I? Use Python with ❤ since 2014. "

    Live in Berlin since 2016 $ Always eat 2 desserts at lunch Work with cool folks at SysEleven GmbH
  2. Project Manager's PoV Tests take time to write. And Time

    is Money But I need time to develop New Features. Conclusion Don't write bugs in the first place!
  3. Developer's PoV Tests are useful to catch bugs. But they

    are boring to write. And without tests, my Pull Requests are rejected. Conclusion Write a couple of tests to make your teammates happy.
  4. Why don't we like tests? We don't learn at school

    how to write tests. ??? Unit Vs. Component Vs. Integration Vs. End-to-End Vs. Acceptance Tests ??? It's more fun to write "real" code. Conclusion Nobody really knows how to write proper tests.
  5. Are tests really useful? Remember this legacy codebase you had

    to maintain? How many times did you use Git Blame?
  6. Figura's Law The probability you leave a company is proportional

    to the number of times you use Git Blame.
  7. A Story of Tradeoffs High-Level Tests Catch more bugs (better

    coverage). Not very helpful for debugging. Take more time to set-up. Low-Level Tests Easier to write. Provide useful information for debugging. Fast to run.
  8. Many Tools Available Unittest (from the standard library). Pytest. Nose/Nose2

    (only bugfixes, no new features). Keyword based libraries,
 like RobotFramework.
  9. Unittest Vs. Pytest Unittest import unittest class TestStringMethods(unittest.TestCase): def test_upper(self):

    self.assertEqual('foo'.upper(), 'FOO') Pytest def test_upper(): assert 'foo'.upper() == 'FOO'
  10. RobotFramework *** Settings *** Documentation A test suite for valid

    login. Resource resource.txt *** Test Cases *** Valid Login Open Browser To Login Page Input Username demo Input Password mode Submit Credentials Welcome Page Should Be Open [Teardown] Close Browser
  11. Which tool to use? Unittest: Why using it, when you

    can use Pytest? Pytest: Flexible, modular, no limits. RobotFramework: Never use that please! Made for testers, not developers.
  12. Why and When To validate feature scopes with customers ✍

    Use Behavior-Driven Development. No customer = No acceptance tests -> Too much boilerplate to maintain
  13. Definition Feature: Blog A site where you can publish articles

    Scenario: Publishing an article Given I wrote an article When I go to the article page And I press the publish button Then the article should be published
  14. Implementation from pytest_bdd import scenario, given, when, then @scenario('publish_article.feature', 'Publishing

    an article') def test_publish(): pass @given('I wrote an article') def article(author): ... @when('I go to the article page') def go_to_article(article, browser): ... @when('I press the publish button') def publish_article(browser): ... @then('the article should be published') def article_is_published(article): ...
  15. Configuration @pytest.fixture(scope='session') def splinter_driver_kwargs(): # Run Firefox in headless mode.

    return {'headless': True} @pytest.fixture(scope='session') def splinter_screenshot_dir(): # Save screenshots in case of error. here = PurePath(__file__).parent return str(here / 'screenshots')
  16. Different Scopes End-to-End Tests: the complete application (similar to acceptance

    tests, without BDD). Integration Tests: interactions between 2 systems. Component Tests: individual parts of an application. Unit Tests: functions/methods, etc.
  17. End-to-End Tests Feel free to use Webtest:
 -> compatible with

    every Web Framework @pytest.fixture def app(): # Return a Django/Flask/Pyramid/etc. app. @pytest.fixture def client(app): return webtest.TestApp(app) def test_home_page(client): response = client.get('/') articles = [ article.text.strip() for article in response.html.select('.article-list')] assert articles == [...]
  18. Unit Tests Nothing fancy here. Allow to check every possible

    edge cases. def football_match(team_1, team_2): return ⚽ class TestFootballMatch: def test_victory(self): winner = football_match('france', 'argentina') assert winner == 'france' def test_defeat(self): with pytest.raises(Defeat): football_match('germany', 'korea')
  19. Pimp My Stub People like mocks Because mocks are easy

    Which makes tests dependant of the implementation But tests should check a behavior,
 not an implementation
  20. Writing a Stub Definition class BankAccount: def send_payment(self, money): ...

    def get_money(self): ... Interface Test class TestBankAccount: def test_send_payment(self, bank): bank.send_payment(100) assert bank.get_money() == 100 class BankAccountStub: def __init__(self): self.payments = [] def send_payment(self, money): self.payments.append(money) def get_money(self): return sum(self.payments) Using the Stub def test_buy_something(self, bank): assert bank.get_money() == 50 buy_something(25, using=bank) assert bank.get_money() == 25
  21. Configuring Pytest def pytest_generate_tests(metafunc): if 'bank' in metafunc.fixturenames: if metafunc.module.__name__

    == 'test_bank_account': params = [BankAccountStub, BankAccount] else: params = [BankAccountStub] metafunc.parametrize('bank', params, indirect=True) @pytest.fixture def bank(request): bank_account = request.param() bank_account.connect() yield bank_account.cancel_recent_transactions()
  22. Workflow 1. Launch the Web App in Docker. 2. Set-up

    the full stack with Docker Compose. 3. Run tests with Tox and Invoke.
  23. Dockerfile FROM python:3-alpine # Install system dependencies. RUN apk update

    && apk add ... RUN pip install tox # Install the Web App. ADD . /pyconweb/ RUN pip install -r /pyconweb/requirements.txt WORKDIR /pyconweb
  24. Docker Compose version: "3" services: web_app: build: . command: flask

    run environment: FLASK_APP: pyconweb FLASK_RUN_HOST: 0.0.0.0 ports: - 5000:5000 volumes: # Share source code with the container. - .:/pyconweb depends_on: - database database: image: postgres:10 ports: - 5432:5432 environment: POSTGRES_PASSWORD: password POSTGRES_USER: user
  25. Tox [tox] envlist = lint, py36, security [testenv] deps =

    coverage pytest webtest -rrequirements.txt commands = coverage run -m pytest {posargs} tests/ coverage report [testenv:dev] basepython = python3.6 commands = deps = {[testenv]deps} {[testenv:lint]deps} envdir = venv usedevelop = True [testenv:lint] deps = flake8 flake8-bugbear flake8-commas flake8-docstrings flake8-import-order flake8-per-file-ignores mccabe pep8-naming commands = flake8 {posargs} pyconweb/ setup.py tests/ usedevelop = True [testenv:security] deps = safety -rrequirements.txt commands = safety check {posargs} --full-report usedevelop = True
  26. Invoke from invoke import task @task() def test(ctx): # $

    invoke test cmdline = ( 'docker-compose run pyconweb ' 'tox -e py36') # Allocate a pseudo-TTY to get colors. ctx.run(cmdline, pty=True, echo=True) @task() def demo(ctx): # $ invoke demo cmdline = 'docker-compose up' ctx.run(cmdline, pty=True)