- Fix line length issues in documentation files - Add language specifications to code blocks - Resolve duplicate heading in README.md - Ensure all markdown files follow best practices
628 lines
15 KiB
Markdown
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
|
|
```
|