4NK_IA_front/docs/TESTING.md
Nicolas Cantu afb58ef4b1 docs: update complete documentation
- Update README.md with comprehensive project documentation
- Update CHANGELOG.md with detailed version 0.1.0 features
- Add ARCHITECTURE.md with technical architecture details
- Add API.md with complete API documentation
- Add DEPLOYMENT.md with deployment guides and configurations
- Add TESTING.md with testing strategies and examples
- Fix markdownlint issues across all documentation files
- Ensure all documentation follows markdown best practices
2025-09-10 18:47:09 +02:00

628 lines
15 KiB
Markdown

# 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(
<Provider store={store}>
<BrowserRouter>
{ui}
</BrowserRouter>
</Provider>
)
}
describe('Layout', () => {
it('should render the application title', () => {
renderWithProviders(<Layout><div>Test content</div></Layout>)
expect(screen.getByText('4NK IA - Front Notarial')).toBeInTheDocument()
})
it('should render navigation tabs', () => {
renderWithProviders(<Layout><div>Test content</div></Layout>)
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(
<Provider store={store}>
<BrowserRouter>
{ui}
</BrowserRouter>
</Provider>
)
}
describe('UploadView', () => {
it('should render upload area', () => {
renderWithProviders(<UploadView />)
expect(screen.getByText(/glisser-déposer/i)).toBeInTheDocument()
})
it('should handle file upload', async () => {
const user = userEvent.setup()
renderWithProviders(<UploadView />)
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(<UploadView />)
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(
<Provider store={store}>
<BrowserRouter>
{ui}
</BrowserRouter>
</Provider>
)
}
describe('Navigation', () => {
it('should navigate between tabs', async () => {
const user = userEvent.setup()
renderWithProviders(<App />)
// 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(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)
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(
<Provider store={store}>
<BrowserRouter>
<UploadView />
</BrowserRouter>
</Provider>
)
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(
<Provider store={store}>
<BrowserRouter>
<Layout>
<div>Test content</div>
</Layout>
</BrowserRouter>
</Provider>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('should have proper ARIA labels', () => {
render(
<Provider store={store}>
<BrowserRouter>
<Layout>
<div>Test content</div>
</Layout>
</BrowserRouter>
</Provider>
)
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
```