Exercices — Module 09

Testing Unit & E2E · 5 exercices pratiques

Ex 01 ⭐ Facile

Tests unitaires d'un service

Écrivez les tests unitaires complets pour un AuthService avec les méthodes validateUser et login. Mockez le repository et le JwtService. Couvrez les cas happy path et error path.

Voir la solution
describe('AuthService', () => {
  let service: AuthService;
  let usersService: jest.Mocked<UsersService>;
  let jwtService: jest.Mocked<JwtService>;

  const mockUser = { id: 1, email: 'test@test.com', password: 'hashed', role: 'user' } as User;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        AuthService,
        { provide: UsersService, useValue: { findByEmail: jest.fn(), findOne: jest.fn() } },
        { provide: JwtService, useValue: { sign: jest.fn(), signAsync: jest.fn() } },
      ],
    }).compile();

    service = module.get(AuthService);
    usersService = module.get(UsersService);
    jwtService = module.get(JwtService);
  });

  describe('validateUser', () => {
    it('should return user when credentials are valid', async () => {
      usersService.findByEmail.mockResolvedValue(mockUser);
      jest.spyOn(bcrypt, 'compare').mockResolvedValue(true as never);
      const result = await service.validateUser('test@test.com', 'password');
      expect(result).toEqual(mockUser);
    });

    it('should return null when user not found', async () => {
      usersService.findByEmail.mockResolvedValue(null);
      const result = await service.validateUser('bad@test.com', 'password');
      expect(result).toBeNull();
    });

    it('should return null when password is wrong', async () => {
      usersService.findByEmail.mockResolvedValue(mockUser);
      jest.spyOn(bcrypt, 'compare').mockResolvedValue(false as never);
      const result = await service.validateUser('test@test.com', 'wrongpass');
      expect(result).toBeNull();
    });
  });

  describe('login', () => {
    it('should return access token', async () => {
      jwtService.sign.mockReturnValue('mock-jwt-token');
      const result = await service.login(mockUser);
      expect(result).toHaveProperty('accessToken');
      expect(result.accessToken).toBe('mock-jwt-token');
    });
  });
});
Ex 02 ⭐ Facile

Tests d'un controller avec Guards mockés

Testez un UsersController en mockant le JwtAuthGuard. Vérifiez que les méthodes appelent bien le service avec les bons arguments et retournent la bonne réponse.

Voir la solution
describe('UsersController', () => {
  let controller: UsersController;
  let service: jest.Mocked<UsersService>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [{
        provide: UsersService,
        useValue: { findAll: jest.fn(), findOne: jest.fn(), create: jest.fn() },
      }],
    })
    .overrideGuard(JwtAuthGuard).useValue({ canActivate: () => true })
    .compile();

    controller = module.get(UsersController);
    service = module.get(UsersService);
  });

  it('findAll should call service.findAll()', async () => {
    service.findAll.mockResolvedValue([]);
    const result = await controller.findAll();
    expect(service.findAll).toHaveBeenCalledTimes(1);
    expect(result).toEqual([]);
  });

  it('create should pass dto to service', async () => {
    const dto = { name: 'Alice', email: 'a@test.com', password: 'Pass1!' };
    const mockUser = { id: 1, ...dto } as User;
    service.create.mockResolvedValue(mockUser);

    const result = await controller.create(dto as any);
    expect(service.create).toHaveBeenCalledWith(dto);
    expect(result).toEqual(mockUser);
  });
});
Ex 03 ⭐⭐ Moyen

Test E2E — flux d'authentification

Écrivez un test E2E complet qui teste le flux complet : register → login → accès route protégée → refresh → logout.

Voir la solution
describe('Auth Flow (e2e)', () => {
  let app: INestApplication;
  let accessToken: string;
  let refreshToken: string;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({ imports: [AppModule] }).compile();
    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
    await app.init();
  });

  afterAll(() => app.close());

  it('POST /auth/register — should create a new user', async () => {
    const res = await request(app.getHttpServer())
      .post('/auth/register')
      .send({ name: 'Alice', email: 'alice@e2e.com', password: 'Alice123!' })
      .expect(201);
    expect(res.body).toHaveProperty('id');
  });

  it('POST /auth/login — should return tokens', async () => {
    const res = await request(app.getHttpServer())
      .post('/auth/login')
      .send({ email: 'alice@e2e.com', password: 'Alice123!' })
      .expect(200);
    expect(res.body).toHaveProperty('accessToken');
    expect(res.body).toHaveProperty('refreshToken');
    accessToken = res.body.accessToken;
    refreshToken = res.body.refreshToken;
  });

  it('GET /auth/me — should return current user with token', () => {
    return request(app.getHttpServer())
      .get('/auth/me')
      .set('Authorization', `Bearer ${accessToken}`)
      .expect(200)
      .expect(res => expect(res.body.email).toBe('alice@e2e.com'));
  });

  it('GET /auth/me — should return 401 without token', () => {
    return request(app.getHttpServer()).get('/auth/me').expect(401);
  });

  it('POST /auth/refresh — should return new tokens', async () => {
    const res = await request(app.getHttpServer())
      .post('/auth/refresh')
      .set('Authorization', `Bearer ${refreshToken}`)
      .expect(200);
    expect(res.body).toHaveProperty('accessToken');
  });
});
Ex 04 ⭐⭐ Moyen

Test d'un interceptor

Testez unitairement un TransformInterceptor. Vérifiez que la réponse est bien enveloppée dans { data, statusCode, timestamp } et que les observables sont correctement gérés.

Voir la solution
import { of } from 'rxjs';

describe('TransformInterceptor', () => {
  let interceptor: TransformInterceptor<any>;

  const mockContext = {
    switchToHttp: () => ({
      getRequest: () => ({ url: '/test' }),
      getResponse: () => ({ statusCode: 200 }),
    }),
  } as ExecutionContext;

  beforeEach(() => {
    interceptor = new TransformInterceptor();
  });

  it('should wrap response in API format', done => {
    const handler = { handle: () => of({ id: 1, name: 'Test' }) } as CallHandler;

    interceptor.intercept(mockContext, handler).subscribe(result => {
      expect(result).toHaveProperty('data');
      expect(result).toHaveProperty('statusCode', 200);
      expect(result).toHaveProperty('timestamp');
      expect(result.data).toEqual({ id: 1, name: 'Test' });
      done();
    });
  });

  it('should handle array responses', done => {
    const handler = { handle: () => of([1, 2, 3]) } as CallHandler;

    interceptor.intercept(mockContext, handler).subscribe(result => {
      expect(Array.isArray(result.data)).toBe(true);
      expect(result.data).toHaveLength(3);
      done();
    });
  });
});
Ex 05 ⭐⭐⭐ Difficile

Coverage 80% minimum

Configurez Jest pour exiger un coverage minimum de 80% sur branches/functions/lines. Ajoutez des tests manquants sur le ProductsService pour atteindre cet objectif.

Voir la solution
// package.json
{
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      },
      "./src/products/": {
        "branches": 90,
        "functions": 95
      }
    },
    "collectCoverageFrom": [
      "src/**/*.ts",
      "!src/**/*.spec.ts",
      "!src/**/*.dto.ts",
      "!src/**/*.entity.ts",
      "!src/main.ts"
    ]
  }
}
// Tests supplémentaires pour couvrir les branches manquantes
describe('ProductsService — edge cases', () => {
  it('findAll should apply pagination correctly', async () => {
    productRepo.findAndCount.mockResolvedValue([[...Array(15)].map((_, i) =>
      ({ ...mockProduct, id: String(i) })
    ), 15]);

    const result = await service.findAll(2, 10);
    expect(result.page).toBe(2);
    expect(result.totalPages).toBe(2);
  });

  it('create should throw ConflictException for duplicate SKU', async () => {
    productRepo.findOneBy.mockResolvedValue(mockProduct);
    await expect(service.create({ ...mockDto, sku: 'EXISTING-SKU' }))
      .rejects.toThrow(ConflictException);
  });

  it('update should throw NotFoundException for missing product', async () => {
    productRepo.findOne.mockResolvedValue(null);
    await expect(service.update('non-existent', {})).rejects.toThrow(NotFoundException);
  });
});
← Cours ▶ Mini-projet Module 10 →