4NK_IA_front/docs/TESTING.md
Nicolas Cantu e69fa95463 fix: resolve remaining markdownlint issues
- 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
2025-09-10 18:47:20 +02:00

15 KiB

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

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

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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

{
  "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

# 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

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

# Générer le rapport
npm run test:coverage

# Ouvrir le rapport HTML
open coverage/index.html