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


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es