Aller au contenu principal

Guide des Tests

Vue d'ensemble

ProjetFrameworkCommande
FrontendJest + Angular Testingnpm test
Video GeneratorJestnpm 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 HTML
  • lcov.info : Format LCOV

Objectifs de Couverture

TypeMinimumIdéal
Statements60%80%
Branches50%70%
Functions60%80%
Lines60%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'));
})
};