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

Recipes for Testing your Web Application

Recipes for Testing your Web Application

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 PyConFR 2018]

Alexandre Figura

October 07, 2018
Tweet

More Decks by Alexandre Figura

Other Decks in Programming

Transcript

  1. Recipes for Testing
 your Web Application Because you deserve the

    best... Alexandre Figura · Site Reliability Engineer @ SysEleven GmbH Committing crimes on Github since 1999 (@arugifa)
  2. Who Am I? 2 • Use Python with since 2014.

    • " Live in Berlin since 2016 $ • Always eat 2 desserts at lunch • Work with cool folks at SysEleven GmbH,
 best company in town.
  3. 5 Project Manager's PoV - "My developers need time to

    write tests." - "And time is money." - "But I need time for developing new features instead..." Conclusion "Just don't write
 bugs in the first place!!"
  4. 6 Developer's PoV - "I think tests are useful to

    catch bugs." - "But they are really boring to write!" - "And without tests, my Pull Requests get rejected..." Conclusion "I write a couple of tests
 to make my teammates happy."
  5. 7 Why People Don't Like Tests? - Nobody taught us

    how to properly write tests. - What is it all about!? Unit tests, Component tests, Integration tests, End-to-End tests, Acceptance Tests, etc. - It's more fun to write real code
  6. 8 To Test or Not To Test? If we don't

    like tests, why bothering about them? - Remember this legacy codebase
 you had to maintain? - How many times did you use Git Blame?
  7. 9 Figura's Law The probability that
 you leave a company

    is proportional to the number of times
 you use Git Blame.
  8. 11 The Theory High-Level Tests Catch more bugs (better coverage).

    Not very helpful for debugging. ⏳ Take more time to set-up. Low-Level Tests Easy to write. Provide useful information
 when debugging. Fast to run.
  9. 12 In Practice... Life is made of tradeoffs. Because time

    is not infinite. Do you prefer: - To discover bugs in live/production? - Or when running tests? - Identify advantages/drawbacks. - Make fully informed choices. - Keep a good balance.
  10. 13 Where To Start From? Acceptance Tests End-to-End Tests Integration

    Tests Component Tests Unit Tests More Work More Coverage Less Pain
  11. 14 Testing Scope - Acceptance tests: the complete application.
 Python

    + Javascript + Network - End-to-End tests: the complete application.
 Python + Javascript + Network - Integration tests: between the application and external systems.
 Python + Network - Component tests: individual parts of an application.
 Python - Unit tests: functions, methods, etc.
 Python
  12. 15 When to write What? - Acceptance tests: when you

    deal directly with customers. - End-to-End tests: when you don't have to write acceptance tests. - Integration tests: when you have time. - Component tests: always. - Unit tests: when you want to enforce data accuracy.
  13. 17 Which Framework to Use? - Unittest: - Not extendable,

    - Not DRY. - Nose: - Not maintained anymore. - Nose 2: - Get only bug fixes.
  14. 18 Which Framework to Use? - Pytest: - Provide lot

    of hooks, - Use assertion introspection, - Lot of extensions available. - RobotFramework: - Hell to maintain, - Designed for testers (not developers).
  15. 20 Pytest Basics Top-Level Directory Subdirectory conftest.py conftest.py Hook Hook

    Fixture Fixture Files Files Test Test Test Tests Fixture Fixture
  16. 22 Idea 1. Write test specifications for a new feature

    ✍ 2.Implement the feature (and the tests) 3 3.Deliver the feature, with green tests People involved when defining tests: - Product Owner, - Customer, - Developer.
  17. 23 Definition 1 Feature: Blog 2 A site where you

    can publish articles 3 4 Scenario: Publishing an article 5 Given I wrote an article 6 When I go to the article page 7 And I press the publish button 8 Then the article should be published ./tests/publish_article.feature
  18. 24 Implementation 1 from pytest_bdd import scenario, given, when, then

    2 3 @scenario("publish_article.feature", "Publishing an article") 4 def test_publish(): 5 pass 6 7 @given("I wrote an article") 8 def article(db): 9 # Save an article into database. 10 11 @when("I go to the article page") 12 def go_to_article(article, browser): 13 # Use a web browser to go to the article page. 14 15 @when("I press the publish button") 16 def publish_article(browser): 17 # Use a web browser to click on the publish button. 18 19 @then("the article should be published") 20 def article_is_published(article): 21 # Check article's state in database. ./tests/test_publish_article.py
  19. 25 Configuration 1 @pytest.fixture(scope='session') 2 def splinter_driver_kwargs(): 3 # Run

    Firefox in headless mode. 4 return {'headless': True} 5 6 7 @pytest.fixture(scope='session') 8 def splinter_screenshot_dir(): 9 # Save screenshots in case of error. 10 here = Path(__file__).parent 11 return str(here / 'screenshots') ./tests/conftest.py
  20. 27 Idea -Same as Acceptance tests. -But without customer meetings

    and boilerplate Tests Application External System
  21. 28 Implementation & Configuration 1 import webtest 2 3 @pytest.fixture

    4 def app(): 5 # Return a Django/Flask/Pyramid WSGI application. 6 7 @pytest.fixture 8 def client(app): 9 return webtest.TestApp(app) # WSGI client 10 11 def test_home_page(client): 12 response = client.get('/') 13 14 articles = [ 15 article.text.strip() 16 for article in response.html.select('.article-list')] 17 18 assert articles == [...] ./tests/test_views.py
  22. 30 Idea -Execute tests with fake objects. -And, when external

    systems are updated, execute interface tests. Tests Application Real
 External System Fake
 External System
  23. 31 Fake's Implementation 1 class BankAccount: 2 def withdraw_money(self, amount):

    3 ... 4 5 def get_balance(self): 6 ... 1 class FakeBankAccount: 2 def __init__(self): 3 self.balance = 0 4 self.withdrawals = [] 5 6 def withdraw_money(self, amount): 7 self.withdrawals.append(amount) 8 9 def get_balance(self): 10 return self.balance - sum(self.withdrawals) ./app/billing.py ./app/fakes.py
  24. 32 Configuration 1 def pytest_generate_tests(metafunc): 2 if 'bank' in metafunc.fixturenames:

    3 if metafunc.module.__name__ == 'test_interface': 4 params = [FakeBankAccount, BankAccount] 5 else: 6 params = [FakeBankAccount] 7 8 metafunc.parametrize('bank', params, indirect=True) 9 10 @pytest.fixture 11 def bank(request): 12 bank_account = request.param() 13 bank_account.connect() 14 yield 15 bank_account.cancel_recent_transactions() ./tests/conftest.py
  25. 33 Application & Interface Tests 1 def test_buy_something(bank): 2 assert

    bank.get_balance() == 50 3 buy_something(25, using=bank) 4 assert bank.get_balance() == 25 1 class TestBankAccount: 2 def test_withdraw_money(self, bank): 3 bank.withdraw_money(100) 4 assert bank.get_balance() == -100 ./tests/test_app.py ./tests/test_interface.py
  26. 35 Idea -When writing Unit tests, -You don't test functions

    or methods. -You test units of behavior - Component tests can be seen as large Unit tests. - Or End-to-End tests, but without interactions
 with external systems
  27. 36 Implementation 1 def print_hello_world(): 2 hello = _print_hello() 3

    world = _print_world() 4 return f"{hello}, {world}!" 5 6 def _print_hello(): 7 return "Hello" 8 9 def _print_world(): 10 return "World" ./app/helpers.py 1 def test_hello_world(): 2 assert print_hello_world() == "Hello, World!" ./tests/test_helpers.py
  28. 37 Good To Know Even if you don't check anything

    in your tests, You will still catch Python syntax errors And thus catch 80% of runtime errors.* Free advice given by the Python Testing Foundation. For a healthier life. * totally arbitrary number
  29. 39 Toolbox -Docker: to run the application with its system

    dependencies. -Docker Compose: to set-up the testing environment. -Tox: to install the application with its Python dependencies. -Pytest: to launch the tests.
  30. 40 Overview Tests independent of running platform. Working example available

    on Github. Docker Compose Application Container Source Code Tox +
 Pytest
  31. 40 Overview Tests independent of running platform. Working example available

    on Github. Docker Compose Application Container Source Code Tox +
 Pytest Database
 Container Other
 Container LDAP
 Container