Skip to content

Testing API

The Testing API provides utilities for testing Ascender Framework applications with pytest integration.

Core Components

Ascender Test Lifecycle

Manages the lifecycle of Ascender applications in test environments.

Key Methods: - before_all(): Initialize application before test suite - after_all(): Cleanup after all tests complete
- before_each(): Setup before each test - after_each(): Cleanup after each test

Usage:

import pytest
from ascender.testing import AscenderTestLifecycle

@pytest.fixture(scope="session")
async def app():
    app = await create_application()
    lifecycle = AscenderTestLifecycle(app)
    await lifecycle.before_all()
    yield app
    await lifecycle.after_all()

AscenderTestLifecycle

__init__

__init__(app_module: type[AscModuleRef] | None = None, providers: list[Provider] | None = None) -> None

Ascender Testing Lifecycle Manager, it's mandatory to define it in conftests. Manages lifecycles from beginning of the tests and to end, initializes ascender framework's features and DI.

Parameters:

Name Type Description Default
app_module AppModule | None

The application module to use. Defaults to None.

None
providers list[Provider] | None

The list of providers to use. Defaults to None.

None

Raises:

Type Description
ValueError

If neither app_module nor providers is provided.

begin_session

begin_session(session: Session)

Hook for beginning the entire application session.

before_test

before_test()

Pytest Interop. Hook to run before test.

after_test

after_test()

Pytest Interop. Hook to run after test.

end_session

end_session()

Hook for ending the entire application session.

TestClient

HTTP test client for loading controllers, dependencies, guards and many other declarations and providers. Universal mock testing client.

__new__

__new__(_framework_dep: type[T], mocks: Mapping[str | type, object | MockDependency | type]) -> T

Create a new instance of the test client.

A new instance with injected mocks for the specified dependencies on the framework dependency injection requirements.

from ascender.testing import TestClient, MockDependency

my_controller = TestClient(
    MyController,
    mocks={
        MyService: MockDependency(
            get_data=lambda self: "mocked data"
        )
    }
)

Parameters:

Name Type Description Default
_framework_dep type[T]

The framework dependency to instantiate.

required
mocks Mapping[str | type, object | MockDependency | type]

A mapping of dependencies to their mock implementations.

required

Returns:

Name Type Description
T T

An instance of the framework dependency with mocks applied.

create_testing_module staticmethod

create_testing_module(imports: Sequence[type[AscModuleRef | ControllerRef]], declarations: Sequence[type[Any]], providers: MutableSequence[Provider], exports: Sequence[type[Any] | str], name: str | None = None) -> type[AscModuleRef]

Create a testing AscModule with the given imports, declarations, providers, and exports.

Parameters:

Name Type Description Default
imports Sequence[type[AscModuleRef | ControllerRef]]

The modules or controllers to import.

required
declarations Sequence[type[Any]]

The declarations for the module.

required
providers MutableSequence[Provider]

The providers for the module.

required
exports Sequence[type[Any] | str]

The exports for the module.

required
name str | None

The name of the testing module (used as object name). Defaults to None.

None

Returns:

Type Description
type[AscModuleRef]

type[AscModuleRef]: The created testing module reference.

Mixer

Pydantic model data generator with optional Faker integration and auto-inference by field name.

Parameters:

Name Type Description Default
extra_generators dict[Any, Callable[..., Any]] | None

Mapping of custom generators (type → callable).

None
enable_auto_faker bool

if True, guesses faker generator from field name.

False
extra_faker_map dict[str, str] | None

additional field name→faker method mapping.

None

gen_by_type

gen_by_type(tp: type[T] | Any) -> T | Any

Generate data based on the provided type.

Parameters:

Name Type Description Default
tp type[T] | Any

The type to generate data for.

required

Returns:

Type Description
T | Any

T | Any: The generated data.

register

register(tp: Any, generator: Callable[..., Any]) -> None

Register your own generator for a specific type.

Parameters:

Name Type Description Default
tp Any

The type to register the generator for.

required
generator Callable[..., Any]

The generator function.

required

FakerField

Marks a field for faker-based data generation.

Example:

Annotated[str, FakerField("name")]

__init__

__init__(method: str, *args, **kwargs)

MockDependency

__init__

__init__(**values) -> None

A lightweight mock utility for simulating dependencies in Ascender Framework tests.

MockDependency lets you create simple mock objects with dynamic attributes or methods, without requiring external mocking libraries.

These mocks can be used in Ascender's dependency injection system (via TestClient or TestInjector), or as standalone mock objects.

Example:

mock_dep = MockDependency(
    method=lambda self: "mocked method",
    value=42
)
mock_obj = mock_dep._as_object("MyMock")

print(mock_obj.method())  # -> "mocked method"
print(mock_obj.value)     # -> 42

Attributes:

Name Type Description
values dict[str, Any]

A mapping of attribute names to their mocked values or callables.

Use Case
  • Designed for mocking Ascender Framework services or injectables in testing scenarios.
  • Can also be used for lightweight mocking in any Python context.

Example Usage

Basic Test Setup

# src/tests/conftest.py
import pytest
from ascender.testing import AscenderTestLifecycle


lifecycle = AscenderTestLifecycle(providers=[])


def pytest_sessionstart(session: pytest.Session):
    """
    Initialize Ascender Framework testing lifecycle at the start of the pytest session.
    """
    lifecycle.begin_session(session)


@pytest.fixture(scope="function", autouse=True)
def ascender_app():
    """
    Lifecycle-managed Ascender Framework test fixture.
    Automatically runs before and after each test function.
    """
    lifecycle.before_test()

    yield lifecycle.application

    lifecycle.after_test()


def pytest_sessionfinish(session: pytest.Session, exitstatus: int):
    """
    Finalize Ascender Framework testing lifecycle at the end of the pytest session.
    """
    lifecycle.end_session()

Testing Controllers

# tests/test_user_controller.py
import pytest
from ascender.testing import TestClient, MockDependency, Mixer

from controllers.user_controller import UserController
from controllers.user_service import UserService

from dtos.users import UserDTO
from responses.users import UserResponse


@pytest.fixture
def client():
    client = TestClient(UserController, {
        UserService: MockDependency(
            find_by_id=lambda self, user_id: UserResponse(id=user_id, name="Test User"),
            create=lambda self, data: UserResponse(id=1, **data.model_dump())
        )
    })
    yield client

@pytest.mark.asyncio
async def test_get_user(client: UserController):
    """Test getting a user by ID."""
    response = await client.get_user(id=223)

    assert isinstance(response, UserResponse)
    assert response.id == 223

@pytest.mark.asyncio
async def test_create_user(client: UserController):
    """Test creating a new user."""
    response = await client.create_user(data=Mixer(enable_auto_faker=True).blend(UserDTO))

    assert response.status_code == 201
    assert "id" in response.json()

Using Mixer for Data Generation

from ascender.testing import Mixer, FakerField
from pydantic import BaseModel

class User(BaseModel):
    name: str
    email: str
    age: int

# Generate random user data
user_data = Mixer().blend(User)
print(user_data)  # User(name='John Doe', email='john@example.com', age=25)

# Generate with specific fields
user = Mixer().blend(User, name="Alice", age=30)
print(user)  # User(name='Alice', email='alice@example.com', age=30)

# Use Faker for specific patterns
class Product(BaseModel):
    name: str = FakerField("company")
    price: float = FakerField("pydecimal", left_digits=3, right_digits=2)

product = Mixer().blend(Product)

products = Mixer().blend_many(Product, 5)

print(len(products))  # 5

mixed = Mixer().blend_multiple([User, Product], count=3)

print(mixed)  # List of mixed User and Product instances
print(len(mixed)) # 3

Running Tests

# Run all tests
ascender run tests run

# Run specific test file
ascender run tests run --path src/tests/test_user_controller.py

# Run specific mark
ascender run tests run -m asyncio

# Run with coverage
pytest --cov=src --cov-report=html

# Run specific test
pytest src/tests/test_user_controller.py::test_get_user

You can use either ascender run tests run or pytest directly to execute your tests.

See Also