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
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
Overview
PyNest has no testing utilities. The existing test suite uses raw
pytestwith manual class instantiation, bypassing the DI container entirely. This means: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()
```
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()
```
TestingModulereference — 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
PyNestTestingModuleclass innest/testing/__init__.pycreate_testing_module(imports, providers, controllers)static factoryTestingModuleBuilderwith.override_provider(token)fluent chain (.use_value,.use_class,.use_factory).override_guard(guard).use_value(mock)to bypass guards in testsTestingModule.get(token)to retrieve instances from the test containerTestingModule.create_http_client()returns anhttpx.AsyncClientfor in-process HTTP testingTestingModule.init()andTestingModule.close()lifecycle supportuse_auto_mock()that replaces all providers withAsyncMockhttpxadded as optional dependency (pip install pynest-api[testing])Dependencies
module_ref.init()andmodule_ref.close()require thisRelated