Guide de test
Stratégie de test, outils, patterns et conventions pour le monorepo nAIxus.
nAIxus utilise une stratégie de test à plusieurs niveaux. Ce guide couvre les outils, les conventions et la structure des tests pour le backend et le frontend.
Vue d'ensemble
| Couche | Framework | Cible | Commande |
|---|---|---|---|
| Backend unit | pytest + pytest-asyncio | Domaine, use cases, executors | cd services/core && uv run pytest tests/unit |
| Backend intégration | pytest | Routes API, repositories avec DB | cd services/core && uv run pytest tests/integration |
| Frontend unit | Vitest | Composants, hooks, utilitaires | pnpm test |
| Frontend E2E | Playwright | Scénarios utilisateur complets | pnpm test:e2e |
Backend (Python)
Structure des tests
services/core/tests/
├── conftest.py # Fixtures partagées
├── unit/
│ ├── domain/ # Tests des entités et règles domaine
│ ├── application/ # Tests des use cases et executors
│ └── infrastructure/ # Tests des adapters (mocks de DB)
├── integration/ # Tests API complets avec DB
├── fixtures/ # Builders de données de test
└── mocks/ # Implémentations mock des portsFixtures principales
Le conftest.py racine fournit :
| Fixture | Type | Rôle |
|---|---|---|
db_session | AsyncSession | Session SQLite in-memory pour les tests |
di_container | DIContainer | Conteneur avec adapters mockés |
client | AsyncClient | Client HTTP pour tester les routes API |
Écrire un test unitaire
Les tests unitaires testent la logique métier en isolation, sans base de données ni réseau.
import pytest
from application.use_cases.create_flow import CreateFlowUseCase
@pytest.mark.asyncio
async def test_create_flow_with_valid_data(di_container):
use_case = CreateFlowUseCase(
flow_repo=di_container.flow_repo,
logger=di_container.logger,
)
result = await use_case.execute(
tenant_id="test-tenant",
data=CreateFlowInput(name="Mon flow", description="Test"),
)
assert result.name == "Mon flow"
assert result.tenant_id == "test-tenant"Écrire un test d'intégration
Les tests d'intégration testent les routes API avec une vraie base de données (SQLite in-memory).
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_list_flows_returns_empty_initially(client: AsyncClient):
response = await client.get(
"/api/flows",
headers={"X-Tenant-ID": "test-tenant"},
)
assert response.status_code == 200
assert response.json() == []Tester un node executor
@pytest.mark.asyncio
async def test_condition_executor_true_branch():
executor = ConditionExecutor()
context = ExecutionContext(
variables={"score": 0.95}
)
result = await executor.execute(
config={"expression": "{{ score }} > 0.8", "operator": ">"},
context=context,
)
assert result.branch == "true"Mocker un fournisseur LLM
Les tests ne doivent jamais appeler un vrai LLM. Utilisez les mocks dans tests/mocks/ :
@pytest.mark.asyncio
async def test_agent_executor_with_mock_llm(mock_llm_port):
mock_llm_port.complete.return_value = LLMResponse(
content="Réponse mockée",
tokens_used={"prompt": 10, "completion": 5},
)
executor = AgentNodeExecutor(llm_port=mock_llm_port)
result = await executor.execute(config={...}, context={...})
assert "Réponse mockée" in result.outputLancer les tests backend
cd services/core
# Tous les tests
uv run pytest
# Tests unitaires seulement
uv run pytest tests/unit
# Tests d'intégration seulement
uv run pytest tests/integration
# Avec couverture
uv run pytest --cov=src --cov-report=html
# Un fichier spécifique
uv run pytest tests/unit/application/test_create_flow.py -vFrontend (TypeScript)
Structure des tests
apps/console_admin/tests/
├── unit/ # Tests de composants et hooks
├── e2e/ # Tests Playwright
└── setup.ts # Configuration VitestÉcrire un test Vitest
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { FlowCard } from '@/features/flows/components/FlowCard';
describe('FlowCard', () => {
it('should display the flow name', () => {
render(<FlowCard name="Mon flow" description="Description" />);
expect(screen.getByText('Mon flow')).toBeInTheDocument();
});
});Tests E2E (Playwright)
import { test, expect } from '@playwright/test';
test('should create a new flow', async ({ page }) => {
await page.goto('/flows');
await page.click('text=New Flow');
await page.fill('[name="flow-name"]', 'Test Flow');
await page.click('text=Create');
await expect(page.getByText('Test Flow')).toBeVisible();
});Lancer les tests frontend
# Tests unitaires (Vitest)
pnpm test
# Tests E2E (Playwright)
pnpm test:e2e
# Avec couverture
pnpm test -- --coverageConventions de test
Nommage
- Backend :
test_<action>_<condition>→test_create_flow_with_duplicate_name_raises_error - Frontend :
should <expected behavior>→should display error when form is invalid
Structure AAA
Tous les tests suivent le pattern Arrange-Act-Assert :
# Arrange — préparer les données et dépendances
flow_data = CreateFlowInput(name="Test")
# Act — exécuter l'action testée
result = await use_case.execute(tenant_id="t1", data=flow_data)
# Assert — vérifier le résultat
assert result.name == "Test"Ce qu'il faut tester
| Toujours tester | Ne pas tester |
|---|---|
| Use cases (logique métier) | Getters/setters triviaux |
| Node executors (comportement) | Configuration Pydantic (validée par le framework) |
| Expressions et conditions | Wiring du DI container |
| Erreurs et cas limites | Code tiers (SQLAlchemy, FastAPI) |
| Routes critiques (auth, CRUD) | Implementation details internes |
Couverture cible
| Couche | Cible |
|---|---|
| Domaine | 95%+ |
| Application (use cases) | 90%+ |
| Infrastructure (adapters) | 80%+ |
| Frontend (composants critiques) | 80%+ |
Pour aller plus loin
- Standards de code — Conventions à respecter
- Architecture backend — Comprendre ce qu'on teste