Skip to content

Feature: Testing Module (PyNestTestingModule, mock providers, test utilities) #118

@ItayTheDar

Description

@ItayTheDar

Overview

PyNest has no testing utilities. The existing test suite uses raw pytest with manual class instantiation, bypassing the DI container entirely. This means:

  • Tests cannot verify that the module graph resolves correctly
  • Mocking a dependency requires monkey-patching, not DI override
  • There is no way to boot a partial module graph for integration tests
  • Guard, interceptor, and pipe behavior is untestable without spinning up a full HTTP server

This feature request proposes a PyNestTestingModule — a first-class testing toolkit modeled after NestJS's @nestjs/testing.


Motivation

```python

Today — manual wiring, no DI, brittle:

def test_user_service():
service = UserService.new(UserService)
service.repo = MockUserRepo()
result = service.get_users()
assert result == []

With PyNestTestingModule — real DI container, swappable mocks:

async def test_user_service():
module = await PyNestTestingModule.create_testing_module(
providers=[UserService],
imports=[DatabaseModule],
).override_provider(UserRepository).use_value(MockUserRepository()).compile()

service = module.get(UserService)
result = await service.get_users()
assert result == []

```


Proposed API

PyNestTestingModule.create_testing_module(metadata)

```python
from nest.testing import PyNestTestingModule

module_ref = await (
PyNestTestingModule
.create_testing_module(
imports=[UserModule],
providers=[LoggerService],
controllers=[UserController],
)
.compile()
)
```

.override_provider(token).use_value(value) — replace with instance

```python
module_ref = await (
PyNestTestingModule
.create_testing_module(imports=[UserModule])
.override_provider(UserRepository)
.use_value(MockUserRepository())
.compile()
)
```

.override_provider(token).use_class(cls) — replace with different class

```python
.override_provider(EmailService).use_class(MockEmailService)
```

.override_provider(token).use_factory(factory) — replace with factory function

```python
.override_provider(ConfigService).use_factory(lambda: FakeConfigService({"db": "sqlite://"}))
```

.override_guard(guard).use_value(mock_guard) — bypass guards

```python
.override_guard(AuthGuard).use_value(AlwaysPassGuard())
```

module_ref.get(token) — retrieve an instance

```python
user_service = module_ref.get(UserService)
config = module_ref.get(ConfigService)
```

module_ref.create_http_client() — in-process HTTP test client

```python
from httpx import AsyncClient

client = module_ref.create_http_client()

async def test_create_user():
response = await client.post("/users", json={"name": "Alice"})
assert response.status_code == 201
assert response.json()["name"] == "Alice"
```

Uses httpx.AsyncClient(app=fastapi_app, base_url="http://test") under the hood — no network required.


TestingModuleBuilder — fluent builder

```python
builder = PyNestTestingModule.create_testing_module(metadata)

Chainable:

builder.override_provider(A).use_value(mock_a)
builder.override_provider(B).use_class(MockB)
builder.override_guard(AuthGuard).use_value(NoopGuard())

module_ref = await builder.compile()
```


TestingModule reference — lifecycle control

```python
module_ref = await builder.compile()

Run setup hooks (OnModuleInit etc.)

await module_ref.init()

After tests:

await module_ref.close()
```


pytest fixtures pattern

```python
import pytest_asyncio
from nest.testing import PyNestTestingModule

@pytest_asyncio.fixture
async def user_module():
module = await (
PyNestTestingModule
.create_testing_module(imports=[UserModule])
.override_provider(UserRepository)
.use_class(InMemoryUserRepository)
.compile()
)
yield module
await module.close()

async def test_list_users(user_module):
service = user_module.get(UserService)
users = await service.list()
assert users == []

async def test_create_user_http(user_module):
async with user_module.create_http_client() as client:
r = await client.post("/users", json={"name": "Bob"})
assert r.status_code == 201
```


Auto-mock support

```python
module_ref = await (
PyNestTestingModule
.create_testing_module(imports=[UserModule])
.use_auto_mock() # all providers replaced with unittest.mock.AsyncMock
.compile()
)

repo_mock = module_ref.get(UserRepository) # is an AsyncMock
repo_mock.find_all.return_value = [fake_user]
```


Acceptance Criteria

  • PyNestTestingModule class in nest/testing/__init__.py
  • create_testing_module(imports, providers, controllers) static factory
  • TestingModuleBuilder with .override_provider(token) fluent chain (.use_value, .use_class, .use_factory)
  • .override_guard(guard).use_value(mock) to bypass guards in tests
  • TestingModule.get(token) to retrieve instances from the test container
  • TestingModule.create_http_client() returns an httpx.AsyncClient for in-process HTTP testing
  • TestingModule.init() and TestingModule.close() lifecycle support
  • use_auto_mock() that replaces all providers with AsyncMock
  • Works with pytest and pytest-asyncio
  • httpx added as optional dependency (pip install pynest-api[testing])
  • Full documentation page with pytest fixture patterns
  • At least 10 example tests covering service, controller, guard, and pipe scenarios

Dependencies

  • Features update readme #1–6 all benefit from this; testing module should be built to cover them
  • Lifecycle Hooks (Feature Add official docs #2) — module_ref.init() and module_ref.close() require this

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions