Test levels
Table of contents
Introduction
A feature’s tests live in levels, from fast tests with everything mocked to a real browser against a running server. Write most of yours at the lower levels, and a few at the top.
tests/
├── unit/
├── integration/
├── functional/
├── e2e/
└── load/
Writing a unit test
Mock everything external. No database, no app, no HTTP.
# splent_feature_notes/tests/unit/test_services.py
from unittest.mock import MagicMock
def test_published_asks_the_repository():
repo = MagicMock()
repo.get_published.return_value = ["a note"]
service = NotesService(notes_repository=repo)
assert service.published() == ["a note"]
repo.get_published.assert_called_once_with()
Writing an integration test
Use the real test database to check repositories and queries. test_client gives a clean database per test, and you work inside the app context.
# splent_feature_notes/tests/integration/test_repositories.py
def test_only_published_notes_are_listed(test_client):
with test_client.application.app_context():
db.session.add(Notes(title="Draft", content="Not yet", published=False))
db.session.add(Notes(title="Out", content="Read me", published=True))
db.session.commit()
found = NotesRepository().get_published()
assert [note.title for note in found] == ["Out"]
Writing a functional test
Send HTTP requests through Flask’s test client, with the whole stack active, and assert on status codes, content and redirects.
# splent_feature_notes/tests/functional/test_notes_pages.py
def test_a_published_note_is_listed(test_client, sample_note):
response = test_client.get("/notes/", follow_redirects=True)
assert response.status_code == 200
assert b"First note" in response.data
def test_writing_a_note_requires_login(test_client):
response = test_client.get("/notes/create")
assert response.status_code == 302
assert "login" in response.headers.get("Location", "").lower()
To act as a signed-in user, use the auth helpers.
from splent_framework.helpers.test_helpers_auth import login, logout
def test_protected_route(test_client):
login(test_client, "user@example.com", "secret")
assert test_client.get("/notes/create").status_code == 200
logout(test_client)
Writing an E2E test
Drive a real browser through Selenium, against a running server.
# splent_feature_notes/tests/e2e/test_browser.py
from splent_framework.environment.host import get_host_for_selenium_testing
from splent_framework.selenium.common import initialize_driver, close_driver
@pytest.fixture()
def browser():
driver = initialize_driver()
yield driver
close_driver(driver)
def test_index_page_loads(browser):
browser.get(f"{get_host_for_selenium_testing()}/notes")
assert "notes" in browser.page_source.lower()
Run it with splent feature:test --e2e. initialize_driver() launches a local Chrome, or Firefox with SELENIUM_BROWSER, and attaches to a grid when SELENIUM_GRID_URL is set. With a grid, set SELENIUM_TARGET_URL too, since the browser’s localhost is not your app. See E2E (Selenium).
Seeding data for one file
Override test_client in a file to seed data for that file only.
@pytest.fixture(scope="function")
def test_client(test_client):
with test_client.application.app_context():
db.session.add(Notes(title="Pinned", content="Seeded for this file", published=True))
db.session.commit()
yield test_client
See also
- Fixtures, fixture patterns and scope rules
- Running tests, the commands