Guide des Tests
Vue d'ensemble
| Projet | Framework | Commande |
|---|---|---|
| Frontend | Jest + Angular Testing | npm test |
| Video Generator | Jest | npm test |
Frontend (Angular)
Exécuter les Tests
cd front-dedicaces
# Tous les tests
npm test
# Mode watch
npm run test:watch
# Avec couverture
npm run test:coverage
# Un fichier spécifique
npm test -- --include=**/product.service.spec.ts
Structure des Tests
src/app/
├── services/
│ ├── product.service.ts
│ └── product.service.spec.ts # Tests du service
├── components/
│ ├── message-card/
│ │ ├── message-card.component.ts
│ │ └── message-card.component.spec.ts
Exemple : Test de Service
// product.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { ProductService } from './product.service';
import { SupabaseService } from './supabase.service';
describe('ProductService', () => {
let service: ProductService;
let supabaseMock: jasmine.SpyObj<SupabaseService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('SupabaseService', ['client']);
spy.client = {
from: jasmine.createSpy().and.returnValue({
select: jasmine.createSpy().and.returnValue({
eq: jasmine.createSpy().and.returnValue(
Promise.resolve({ data: [], error: null })
)
})
})
};
TestBed.configureTestingModule({
providers: [
ProductService,
{ provide: SupabaseService, useValue: spy }
]
});
service = TestBed.inject(ProductService);
supabaseMock = TestBed.inject(SupabaseService) as jasmine.SpyObj<SupabaseService>;
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should fetch products', async () => {
const mockProducts = [{ id: 1, name: 'Test' }];
supabaseMock.client.from().select().eq.and.returnValue(
Promise.resolve({ data: mockProducts, error: null })
);
const products = await service.getProducts();
expect(products).toEqual(mockProducts);
});
});
Exemple : Test de Composant
// message-card.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageCardComponent } from './message-card.component';
describe('MessageCardComponent', () => {
let component: MessageCardComponent;
let fixture: ComponentFixture<MessageCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MessageCardComponent]
}).compileComponents();
fixture = TestBed.createComponent(MessageCardComponent);
component = fixture.componentInstance;
});
it('should create', () => {
component.message = {
id: '1',
participant_name: 'Test',
message_media: '<p>Hello</p>',
message_type: 'TEXT'
};
fixture.detectChanges();
expect(component).toBeTruthy();
});
it('should display author name', () => {
component.message = {
id: '1',
participant_name: 'Jean',
message_media: '<p>Hello</p>',
message_type: 'TEXT'
};
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.textContent).toContain('Jean');
});
});
Test de State NGXS
// products.state.spec.ts
import { TestBed } from '@angular/core/testing';
import { NgxsModule, Store } from '@ngxs/store';
import { ProductsState } from './products.state';
import { LoadProducts, CreateProduct } from './products.actions';
describe('ProductsState', () => {
let store: Store;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [NgxsModule.forRoot([ProductsState])]
});
store = TestBed.inject(Store);
});
it('should load products', () => {
const products = [{ id: 1, name: 'Test' }];
store.dispatch(new LoadProducts(products));
const state = store.selectSnapshot(ProductsState.products);
expect(state).toEqual(products);
});
});
Video Generator
Exécuter les Tests
cd dedicace-generator
# Tous les tests
npm test
# Mode watch
npm run test:watch
Exemple de Test
// main.test.js
const request = require('supertest');
const app = require('./main');
describe('Video Generator API', () => {
describe('GET /health', () => {
it('should return healthy status', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('healthy');
});
});
describe('POST /create_presentation', () => {
it('should reject invalid requests', async () => {
const response = await request(app)
.post('/create_presentation')
.send({});
expect(response.status).toBe(400);
expect(response.body.error).toBeDefined();
});
it('should accept valid requests', async () => {
const response = await request(app)
.post('/create_presentation')
.send({
title: 'Test',
pages: [
{ id: 1, type: 'text', content: '<p>Hello</p>', author: 'Test' }
]
});
expect(response.status).toBe(202);
expect(response.body.presentationId).toBeDefined();
});
});
});
Tests E2E (Cypress)
Installation
cd front-dedicaces
npm install -D cypress
Configuration
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:4200',
supportFile: 'cypress/support/e2e.ts',
},
});
Exemple de Test E2E
// cypress/e2e/auth.cy.ts
describe('Authentication', () => {
it('should display login page', () => {
cy.visit('/connexion');
cy.contains('Connexion');
cy.get('input[type="email"]').should('be.visible');
cy.get('input[type="password"]').should('be.visible');
});
it('should login with valid credentials', () => {
cy.visit('/connexion');
cy.get('input[type="email"]').type('test@example.com');
cy.get('input[type="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/espace-personnel');
});
it('should show error with invalid credentials', () => {
cy.visit('/connexion');
cy.get('input[type="email"]').type('wrong@example.com');
cy.get('input[type="password"]').type('wrongpassword');
cy.get('button[type="submit"]').click();
cy.contains('Identifiants incorrects');
});
});
Exécuter les Tests E2E
# Interface graphique
npm run cypress:open
# Mode headless
npm run cypress:run
Couverture de Code
Frontend
npm run test:coverage
Résultat dans coverage/ :
index.html: Rapport HTMLlcov.info: Format LCOV
Objectifs de Couverture
| Type | Minimum | Idéal |
|---|---|---|
| Statements | 60% | 80% |
| Branches | 50% | 70% |
| Functions | 60% | 80% |
| Lines | 60% | 80% |
CI/CD
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: front-dedicaces
run: npm ci
- name: Run tests
working-directory: front-dedicaces
run: npm test -- --no-watch --no-progress --browsers=ChromeHeadless
- name: Build
working-directory: front-dedicaces
run: npm run build
test-generator:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: dedicace-generator
run: npm ci
- name: Run tests
working-directory: dedicace-generator
run: npm test
Bonnes Pratiques
Nommage des Tests
describe('ProductService', () => {
describe('getProducts', () => {
it('should return empty array when no products', () => {});
it('should return products sorted by date', () => {});
it('should throw error on network failure', () => {});
});
});
Arrange-Act-Assert
it('should calculate total correctly', () => {
// Arrange
const items = [
{ price: 10 },
{ price: 20 }
];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(30);
});
Mocks
// Mock simple
const mockService = {
getData: jest.fn().mockResolvedValue([])
};
// Mock avec implémentation
const mockService = {
getData: jest.fn().mockImplementation((id) => {
if (id === 1) return Promise.resolve({ id: 1, name: 'Test' });
return Promise.reject(new Error('Not found'));
})
};