# Guide de tests - 4NK IA Front Notarial ## Vue d'ensemble Ce guide couvre la stratégie de tests pour l'application 4NK IA Front Notarial, incluant les tests unitaires, d'intégration et end-to-end. ## Stack de test ### Outils principaux - **Vitest** : Framework de test rapide et moderne - **Testing Library** : Tests d'intégration React - **JSDOM** : Environnement DOM simulé - **MSW** : Mock Service Worker pour les APIs - **Coverage V8** : Rapport de couverture de code ### Configuration #### vitest.config.ts ```typescript import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', globals: true, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'dist/', '**/*.d.ts', '**/*.config.*', '**/setup.*' ] } } }) ``` #### setup.test.ts ```typescript import { expect, afterEach } from 'vitest' import { cleanup } from '@testing-library/react' import * as matchers from '@testing-library/jest-dom/matchers' // Étendre les matchers de testing-library expect.extend(matchers) // Nettoyer après chaque test afterEach(() => { cleanup() }) ``` ## Types de tests ### Tests unitaires #### Composants React ```typescript // tests/components/Layout.test.tsx import { render, screen } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' import { Provider } from 'react-redux' import { store } from '../../src/store' import { Layout } from '../../src/components/Layout' const renderWithProviders = (ui: React.ReactElement) => { return render( {ui} ) } describe('Layout', () => { it('should render the application title', () => { renderWithProviders(
Test content
) expect(screen.getByText('4NK IA - Front Notarial')).toBeInTheDocument() }) it('should render navigation tabs', () => { renderWithProviders(
Test content
) expect(screen.getByRole('tablist')).toBeInTheDocument() }) }) ``` #### Services API ```typescript // tests/services/api.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest' import { documentApi } from '../../src/services/api' import axios from 'axios' // Mock axios vi.mock('axios') const mockedAxios = vi.mocked(axios) describe('documentApi', () => { beforeEach(() => { vi.clearAllMocks() }) describe('upload', () => { it('should upload a document successfully', async () => { const mockResponse = { data: { id: 'doc_123', name: 'test.pdf', type: 'application/pdf', size: 1024, uploadDate: new Date(), status: 'completed' } } mockedAxios.create.mockReturnValue({ post: vi.fn().mockResolvedValue(mockResponse) } as any) const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }) const result = await documentApi.upload(file) expect(result.id).toBe('doc_123') expect(result.name).toBe('test.pdf') expect(result.status).toBe('completed') }) it('should return demo data on error', async () => { mockedAxios.create.mockReturnValue({ post: vi.fn().mockRejectedValue(new Error('Network error')) } as any) const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }) const result = await documentApi.upload(file) expect(result.id).toMatch(/^demo-/) expect(result.name).toBe('test.pdf') }) }) }) ``` #### Redux Slices ```typescript // tests/store/documentSlice.test.ts import { describe, it, expect } from 'vitest' import documentReducer, { setCurrentDocument } from '../../src/store/documentSlice' import { uploadDocument } from '../../src/store/documentSlice' describe('documentSlice', () => { const initialState = { documents: [], currentDocument: null, extractionResult: null, analysisResult: null, contextResult: null, conseilResult: null, loading: false, error: null } it('should handle setCurrentDocument', () => { const document = { id: 'doc_123', name: 'test.pdf', type: 'application/pdf', size: 1024, uploadDate: new Date(), status: 'completed' as const } const action = setCurrentDocument(document) const newState = documentReducer(initialState, action) expect(newState.currentDocument).toEqual(document) expect(newState.extractionResult).toBeNull() }) it('should handle uploadDocument.pending', () => { const action = { type: uploadDocument.pending.type } const newState = documentReducer(initialState, action) expect(newState.loading).toBe(true) expect(newState.error).toBeNull() }) }) ``` ### Tests d'intégration #### Vues complètes ```typescript // tests/views/UploadView.test.tsx import { render, screen, fireEvent, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' import { store } from '../../src/store' import { UploadView } from '../../src/views/UploadView' const renderWithProviders = (ui: React.ReactElement) => { return render( {ui} ) } describe('UploadView', () => { it('should render upload area', () => { renderWithProviders() expect(screen.getByText(/glisser-déposer/i)).toBeInTheDocument() }) it('should handle file upload', async () => { const user = userEvent.setup() renderWithProviders() const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }) const input = screen.getByLabelText(/sélectionner des fichiers/i) await user.upload(input, file) await waitFor(() => { expect(screen.getByText('test.pdf')).toBeInTheDocument() }) }) it('should display document list after upload', async () => { const user = userEvent.setup() renderWithProviders() const file = new File(['content'], 'test.pdf', { type: 'application/pdf' }) const input = screen.getByLabelText(/sélectionner des fichiers/i) await user.upload(input, file) await waitFor(() => { expect(screen.getByRole('list')).toBeInTheDocument() expect(screen.getByText('test.pdf')).toBeInTheDocument() }) }) }) ``` #### Navigation ```typescript // tests/navigation.test.tsx import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { BrowserRouter } from 'react-router-dom' import { Provider } from 'react-redux' import { store } from '../src/store' import App from '../src/App' const renderWithProviders = (ui: React.ReactElement) => { return render( {ui} ) } describe('Navigation', () => { it('should navigate between tabs', async () => { const user = userEvent.setup() renderWithProviders() // Vérifier que l'onglet Upload est actif par défaut expect(screen.getByText('Upload')).toHaveAttribute('aria-selected', 'true') // Cliquer sur l'onglet Extraction await user.click(screen.getByText('Extraction')) expect(screen.getByText('Extraction')).toHaveAttribute('aria-selected', 'true') expect(screen.getByText('Upload')).toHaveAttribute('aria-selected', 'false') }) }) ``` ### Tests d'API avec MSW #### Configuration MSW ```typescript // tests/mocks/handlers.ts import { http, HttpResponse } from 'msw' export const handlers = [ // Upload de document http.post('/api/documents/upload', () => { return HttpResponse.json({ id: 'doc_123', name: 'test.pdf', type: 'application/pdf', size: 1024, uploadDate: new Date().toISOString(), status: 'completed' }) }), // Extraction de données http.get('/api/documents/:id/extract', () => { return HttpResponse.json({ documentId: 'doc_123', text: 'Texte extrait du document...', language: 'fr', documentType: 'Acte de vente', identities: [ { id: '1', type: 'person', firstName: 'Jean', lastName: 'Dupont', birthDate: '1980-05-15', nationality: 'Française', confidence: 0.95 } ], addresses: [], properties: [], contracts: [], signatures: [], confidence: 0.92 }) }), // Erreur de connexion http.get('/api/documents/:id/analyze', () => { return HttpResponse.error() }) ] ``` #### Tests avec MSW ```typescript // tests/integration/api.test.tsx import { setupServer } from 'msw/node' import { handlers } from '../mocks/handlers' import { documentApi } from '../../src/services/api' const server = setupServer(...handlers) beforeAll(() => server.listen()) afterEach(() => server.resetHandlers()) afterAll(() => server.close()) describe('API Integration', () => { it('should extract document data', async () => { const result = await documentApi.extract('doc_123') expect(result.documentId).toBe('doc_123') expect(result.identities).toHaveLength(1) expect(result.identities[0].firstName).toBe('Jean') }) it('should handle API errors gracefully', async () => { const result = await documentApi.analyze('doc_123') // Devrait retourner des données de démonstration expect(result.documentId).toBe('doc_123') expect(result.credibilityScore).toBeDefined() }) }) ``` ## Tests de performance ### Tests de rendu ```typescript // tests/performance/render.test.tsx import { render } from '@testing-library/react' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' import { store } from '../../src/store' import App from '../../src/App' describe('Performance', () => { it('should render app within acceptable time', () => { const start = performance.now() render( ) const end = performance.now() const renderTime = end - start // Le rendu initial devrait prendre moins de 100ms expect(renderTime).toBeLessThan(100) }) }) ``` ### Tests de mémoire ```typescript // tests/performance/memory.test.tsx import { render, cleanup } from '@testing-library/react' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' import { store } from '../../src/store' import { UploadView } from '../../src/views/UploadView' describe('Memory Management', () => { it('should not leak memory on multiple renders', () => { const initialMemory = (performance as any).memory?.usedJSHeapSize || 0 // Rendre et nettoyer plusieurs fois for (let i = 0; i < 10; i++) { render( ) cleanup() } const finalMemory = (performance as any).memory?.usedJSHeapSize || 0 const memoryIncrease = finalMemory - initialMemory // L'augmentation de mémoire devrait être raisonnable expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024) // 10MB }) }) ``` ## Tests d'accessibilité ### Tests avec jest-axe ```typescript // tests/accessibility/a11y.test.tsx import { render } from '@testing-library/react' import { axe, toHaveNoViolations } from 'jest-axe' import { Provider } from 'react-redux' import { BrowserRouter } from 'react-router-dom' import { store } from '../../src/store' import { Layout } from '../../src/components/Layout' expect.extend(toHaveNoViolations) describe('Accessibility', () => { it('should not have accessibility violations', async () => { const { container } = render(
Test content
) const results = await axe(container) expect(results).toHaveNoViolations() }) it('should have proper ARIA labels', () => { render(
Test content
) expect(screen.getByRole('banner')).toBeInTheDocument() expect(screen.getByRole('tablist')).toBeInTheDocument() }) }) ``` ## Scripts de test ### package.json ```json { "scripts": { "test": "vitest", "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", "test:debug": "vitest --inspect-brk" } } ``` ### Exécution des tests ```bash # Tests en mode watch npm run test # Tests avec interface graphique npm run test:ui # Tests une seule fois npm run test:run # Tests avec couverture npm run test:coverage # Tests en mode debug npm run test:debug ``` ## Configuration CI/CD ### GitHub Actions ```yaml name: Tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '22.12' cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm run test:coverage - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage/lcov.info ``` ## Bonnes pratiques ### Organisation des tests - **Un fichier de test par composant** : `Component.test.tsx` - **Tests groupés par fonctionnalité** : `describe` blocks - **Tests isolés** : Chaque test doit être indépendant - **Noms descriptifs** : `it('should do something specific')` ### Mocking - **Mock des dépendances externes** : APIs, services - **Mock des hooks** : React hooks personnalisés - **Mock des modules** : Modules Node.js ### Assertions - **Assertions spécifiques** : Éviter les assertions génériques - **Tests de régression** : Vérifier les bugs corrigés - **Tests de cas limites** : Valeurs nulles, erreurs ### Performance - **Tests rapides** : Éviter les tests lents - **Parallélisation** : Utiliser `--threads` pour Vitest - **Cache** : Utiliser le cache des dépendances ## Métriques de qualité ### Couverture de code - **Minimum 80%** : Couverture globale - **Minimum 90%** : Composants critiques - **100%** : Fonctions utilitaires ### Types de couverture - **Statements** : Instructions exécutées - **Branches** : Branches conditionnelles - **Functions** : Fonctions appelées - **Lines** : Lignes de code exécutées ### Rapport de couverture ```bash # Générer le rapport npm run test:coverage # Ouvrir le rapport HTML open coverage/index.html ```