nAIxus Docs

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

CoucheFrameworkCibleCommande
Backend unitpytest + pytest-asyncioDomaine, use cases, executorscd services/core && uv run pytest tests/unit
Backend intégrationpytestRoutes API, repositories avec DBcd services/core && uv run pytest tests/integration
Frontend unitVitestComposants, hooks, utilitairespnpm test
Frontend E2EPlaywrightScénarios utilisateur completspnpm 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 ports

Fixtures principales

Le conftest.py racine fournit :

FixtureTypeRôle
db_sessionAsyncSessionSession SQLite in-memory pour les tests
di_containerDIContainerConteneur avec adapters mockés
clientAsyncClientClient 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.output

Lancer 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 -v

Frontend (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 -- --coverage

Conventions 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 testerNe pas tester
Use cases (logique métier)Getters/setters triviaux
Node executors (comportement)Configuration Pydantic (validée par le framework)
Expressions et conditionsWiring du DI container
Erreurs et cas limitesCode tiers (SQLAlchemy, FastAPI)
Routes critiques (auth, CRUD)Implementation details internes

Couverture cible

CoucheCible
Domaine95%+
Application (use cases)90%+
Infrastructure (adapters)80%+
Frontend (composants critiques)80%+

Pour aller plus loin

On this page