Testing

Write unit, integration and functional tests for splent_feature_notes.


Table of contents

  1. Prerequisites
  2. Test structure recap
  3. Step 1. Set up fixtures
  4. Step 2. Write unit tests
  5. Step 3. Write integration tests
  6. Step 4. Write functional tests
  7. Step 5. Run the tests
  8. Step 6. Check dependency consistency
  9. Step 7. Batch test the product
  10. What you learned
  11. Next

Prerequisites

You completed Tutorial 3 and have splent_feature_notes working in my_first_app.


Test structure recap

Every feature has one test directory per level. See Feature testing for the whole model.

tests/
├── conftest.py           # Shared fixtures
├── unit/                 # Mocked dependencies, no DB, no HTTP
│   └── test_services.py
├── integration/          # Real test DB, repositories, services
│   └── test_repositories.py
├── functional/           # Flask test_client, full HTTP cycles
│   └── test_routes.py
├── e2e/                  # Selenium (requires running server)
│   └── test_browser.py
└── load/                 # Locust load tests (feature:test --load)
    └── locustfile.py

Step 1. Set up fixtures

Edit splent_feature_notes/src/splent_io/splent_feature_notes/tests/conftest.py.

from splent_framework.fixtures.fixtures import *  # noqa: F401,F403
import pytest
from splent_io.splent_feature_auth.models import User
from splent_io.splent_feature_notes.models import Notes
from splent_framework.db import db


@pytest.fixture(scope="function")
def auth_test_user(test_client, test_app):
    with test_app.app_context():
        user = User(email="notes_user@example.com", active=True)
        user.set_password("1234")
        db.session.add(user)
        db.session.commit()
        db.session.refresh(user)
        return user


@pytest.fixture(scope="function")
def logged_in_client(test_client, auth_test_user):
    test_client.post(
        "/login",
        data={"email": "notes_user@example.com", "password": "1234"},
        follow_redirects=True,
    )
    return test_client


@pytest.fixture(scope="function")
def sample_note(test_client, test_app, auth_test_user):
    with test_app.app_context():
        note = Notes(
            user_id=auth_test_user.id,
            title="Test Note",
            content="This is a test note.",
        )
        db.session.add(note)
        db.session.commit()
        db.session.refresh(note)
        return note

The first line imports the framework’s shared fixtures, test_app (a Flask app with a test database) and test_client (a Flask test client). Every test of the feature gets them through conftest.py.


Step 2. Write unit tests

Edit splent_feature_notes/src/splent_io/splent_feature_notes/tests/unit/test_services.py.

from unittest.mock import MagicMock
from splent_io.splent_feature_notes.services import NotesService


def test_create_note():
    repo = MagicMock()
    repo.create.return_value = MagicMock(id=1, title="My Note", user_id=1)

    service = NotesService()
    service.repository = repo

    result = service.create(user_id=1, title="My Note", content="Hello")

    repo.create.assert_called_once_with(user_id=1, title="My Note", content="Hello")
    assert result.title == "My Note"


def test_get_by_user():
    mock_note_1 = MagicMock(id=1, title="Note A", user_id=5)
    mock_note_2 = MagicMock(id=2, title="Note B", user_id=5)

    repo = MagicMock()
    repo.get_by_column.return_value = [mock_note_1, mock_note_2]

    service = NotesService()
    service.repository = repo

    results = service.get_by_user(5)

    repo.get_by_column.assert_called_once_with("user_id", 5)
    assert len(results) == 2


def test_delete_note():
    repo = MagicMock()
    repo.delete.return_value = True

    service = NotesService()
    service.repository = repo

    result = service.delete(1)

    repo.delete.assert_called_once_with(1)
    assert result is True

Unit tests mock the repository, so they need no database, no app and no HTTP, and run in milliseconds.


Step 3. Write integration tests

Edit splent_feature_notes/src/splent_io/splent_feature_notes/tests/integration/test_repositories.py.

from splent_io.splent_feature_notes.repositories import NotesRepository


def test_create_note(test_app, auth_test_user):
    with test_app.app_context():
        repo = NotesRepository()
        note = repo.create(
            user_id=auth_test_user.id,
            title="Integration Note",
            content="Created in test DB",
        )

        assert note.id is not None
        assert note.title == "Integration Note"
        assert note.user_id == auth_test_user.id


def test_get_by_column(test_app, auth_test_user):
    with test_app.app_context():
        repo = NotesRepository()
        repo.create(user_id=auth_test_user.id, title="Note 1", content="First")
        repo.create(user_id=auth_test_user.id, title="Note 2", content="Second")

        notes = repo.get_by_column("user_id", auth_test_user.id)

        assert len(notes) == 2
        assert all(n.user_id == auth_test_user.id for n in notes)


def test_delete_note(test_app, auth_test_user):
    with test_app.app_context():
        repo = NotesRepository()
        note = repo.create(
            user_id=auth_test_user.id,
            title="To Delete",
            content="Will be removed",
        )
        note_id = note.id

        result = repo.delete(note_id)

        assert result is True
        assert repo.get_by_id(note_id) is None

Integration tests use the real test database. test_app builds the app once per session, and test_client, which auth_test_user depends on, drops and recreates every table before each test. There is no transaction rollback, so what a test writes stays until the next test resets the tables.


Step 4. Write functional tests

Edit splent_feature_notes/src/splent_io/splent_feature_notes/tests/functional/test_routes.py.

def test_notes_index_requires_login(test_client):
    response = test_client.get("/notes/")
    assert response.status_code == 302
    assert "/login" in response.headers["Location"]


def test_notes_index_authenticated(logged_in_client):
    response = logged_in_client.get("/notes/")
    assert response.status_code == 200
    assert b"My Notes" in response.data


def test_create_note(logged_in_client):
    response = logged_in_client.post(
        "/notes/create",
        data={"title": "Functional Test Note", "content": "Created via test client"},
        follow_redirects=True,
    )
    assert response.status_code == 200
    assert b"Functional Test Note" in response.data


def test_create_note_missing_title(logged_in_client):
    response = logged_in_client.post(
        "/notes/create",
        data={"title": "", "content": "No title provided"},
        follow_redirects=True,
    )
    assert response.status_code == 200
    assert b"Title is required." in response.data


def test_delete_note(logged_in_client, sample_note):
    response = logged_in_client.post(
        f"/notes/{sample_note.id}/delete",
        follow_redirects=True,
    )
    assert response.status_code == 200
    assert b"Note deleted." in response.data
    assert b"Test Note" not in response.data

Functional tests send real HTTP requests through Flask’s test client, exercising routes, templates, services and the database together.


Step 5. Run the tests

Run each level on its own.

splent feature:test notes --unit
splent feature:test notes --integration
splent feature:test notes --functional

Or all three at once, the default.

splent feature:test notes

Each level runs as its own pytest session, and a summary closes the run.

🧪 Running tests for notes [unit, integration, functional]...

  ▶  splent_feature_notes
     unit
...
splent_io/splent_feature_notes/tests/unit/test_services.py ...           [100%]
...
     integration
...
splent_io/splent_feature_notes/tests/integration/test_repositories.py .. [ 66%]
.                                                                        [100%]
...
     functional
...
splent_io/splent_feature_notes/tests/functional/test_routes.py .....     [100%]

============================== 5 passed in 3.85s ===============================

✅ Test runs (feature × level): 3 passed.

-v prints each test name and result.

splent feature:test notes -v

-k filters by test name.

splent feature:test notes -k test_create_note

Step 6. Check dependency consistency

splent product:validate
  Validating my_first_app...

  1. Configuration  (UVL — splent product:validate --only config)
     ✅ Feature selection is satisfiable (10 features)

  2. Compatibility  (contracts — splent product:validate --only compat)
     ✅ No collisions detected
     ℹ️  5 informational note(s)

  3. Dependencies  (imports — splent product:validate --only deps)
     ✅ All imports respect UVL constraints (8 checks)

  ✅ Product is valid.

The third check compares the imports of splent_feature_notes with the UVL constraints. Notes imports User from auth and the UVL declares notes => auth, so it passes. An import from a feature the UVL does not declare as a dependency would be flagged.


Step 7. Batch test the product

With the product selected, test every feature at once.

splent product:test --unit

It runs the unit tests of every feature that has some (auth, profile, public, redis, session_redis, settings, theme and notes) in one pass.

...
  ▶  splent_feature_notes
     unit
...
splent_io/splent_feature_notes/tests/unit/test_services.py ...           [100%]

============================== 3 passed in 0.03s ===============================

✅ Test runs (feature × level): 8 passed.

✅ All 1 product(s) passed.

What you learned

Concept Summary
Unit tests Mock the repository and test service logic alone, without a database
Integration tests Use the real test database to check repository queries
Functional tests Use Flask’s test client to check the routes’ responses
Fixtures test_app and test_client from the framework, custom fixtures in conftest.py
feature:test Run tests by feature and level (--unit, --integration, --functional)
product:validate Checks that Python imports match the UVL dependencies
product:test Runs the tests of every feature in a product, then the product’s own under tests/

Next

Continue with Tutorial 5, Release and deploy, to rehearse the release of your feature and deploy the product.


Back to top

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