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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions