Aller au contenu principal

Tests Frontend

La partie test est assurée par le test runner Karma et la librairie Jasmine fournie par défaut par Angular.
doc Jasmine - your first test suite

npm run test lance les tests en mode watch et relancera les tests après modification du code.

Les tests sont organisés dans des fonctions describe, chaque test est une fonction it qui teste un comportement spécifique.
Il est possible de se concentrer sur l'execution d'un seul test/test-suite en remplaçant describe ou it par fdescribe / fit (f pour focus). Il est également possible d'exclure des tests en utilisant xdescribe / xit.
La fonction describe parente permet de définir les objets à tester/mocker en définissant tout d'abord une méthode beforeEach qui sera exécutée avant chaque test it permettant ainsi de reinitialiser tous les objets servant au test.

Test simple d'un service sans dépendances

describe('RedirectService', () => {
// le service à tester
let service: RedirectService;

// méthode servant à l'initialisation
beforeEach(() => {
// on initiliase le service par son constructeur
service = new RedirectService();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

// fonction englobante se concentrant sur la méthode getRedirect
describe('.getRedirect()', () => {
// premier test d'un scénario pour cette méthode authenticate
it('should initially return default route', () => {
expect(service.getRedirect()).toEqual('/');
});
// second test
it('should return saved route', () => {
service.saveTarget('/test/url');
expect(service.getRedirect()).toEqual('/test/url');
});
});

// regroupe les tests d'une autre méthode du service
describe('.discardTarget()', () => {
it('...
});
});

Tests de fonctions/utils

Il suffit d'importer la fonction à tester.

import { replacePathVariables } from './http-utils';

// nom du fichier utils à tester
describe('http-utils', function () {
// tests de la méthode replacePathVariables
describe('.replacePathVariables(x, y)', function () {
const url = '/events/{idEvent}/type/{type}';
it('should replace all variables in the string x from the y Record', function () {
const params = {
idEvent: '1',
type: 'CONNEXION',
};
expect(replacePathVariables(url, params)).toBe(
'/events/1/type/CONNEXION'
);
});
//... autres tests pour replacePathVariables
});
});

Tests de service avec dépendances

Ici le service CredentialsAuthService a une dépendance AuthApi qui sera mockée par un SpyObj permettant de mocker les interactions et de vérifier les appels à ce mock.

describe('CredentialsAuthService', () => {
// le service à tester
let service: CredentialsAuthService;
// un service qui sera mocké
let authApiSpy: jasmine.SpyObj<AuthApi>;

// méthode servant à l'initialisation
beforeEach(() => {
// définition du mock, dans ce service on appelera seulement la méthode authenticate
const authApiSpyObj = jasmine.createSpyObj<AuthApi>('AuthApi', [
'authenticate',
]);
// on définit notre module de test, on passe le mock en tant que provider pour AuthApi
TestBed.configureTestingModule({
providers: [
{
provide: AuthApi,
useValue: authApiSpyObj,
},
],
});

// récupération des services grâce à l'injecteur Angular afin de rester proche du fonctionnement réél
service = TestBed.inject(CredentialsAuthService);
authApiSpy = TestBed.inject(AuthApi) as jasmine.SpyObj<AuthApi>;
});

it('should be created', () => {
expect(service).toBeTruthy();
});

// fonction englobante se concentrant sur la méthode authenticate
describe('.authenticate()', () => {
// premier test d'un scénario pour cette méthode authenticate
it('should call authenticate API and retrieve token', (done: DoneFn) => {
const credentials = { login: 'test', password: 'test' };
const mockedResponse: TokenResponse = { accessToken: 'fake_token' };
// définition du comportement du mock pour ce test
authApiSpy.authenticate
.withArgs(credentials)
.and.returnValue(of(mockedResponse));

// appel à la méthode testée
service.authenticate(credentials).subscribe((tokenResponse) => {
// vérifications
expect(tokenResponse).toEqual(mockedResponse);
expect(authApiSpy.authenticate).toHaveBeenCalledWith(credentials);

done();
});
});
//... autres tests de la méthode authenticate
});
});
Notes :

Quand on teste le résultat d'un observable comme ici, il vaut mieux appeler la méthode done() passée en paramètre de la fonction it, une fois le traitement terminé.

Ici le mock retourne une valeur synchrone grâce à l'opérateur of, mais la méthode peut être vraiment asynchrone. En rajoutant l'appel à done, on voit tout de suite qu'on est dans une méthode potentiellement asynchrone.

Il est possible d'injecter les dépendances via le constructeur quand cela est nécessaire (ex: une pipe avec des dépendances).
La méthode ci-dessus est recommandée pour les services puisqu'elle utilise le système d'injection d'Angular.

describe('TranslateBooleanPipe', () => {
// pipe avec dépendance
let pipe: TranslateBooleanPipe;
// dépendance mockée
let translateServiceSpy: jasmine.SpyObj<TranslateService>;

beforeEach(function () {
translateServiceSpy = jasmine.createSpyObj<TranslateService>(
'TranslateService',
['instant']
);
// configuration du comportement du mock fixe pour chaque test, on peut donc le définir ici
translateServiceSpy.instant
.withArgs('STANDALONE.PIPES.BOOLEAN_TRANSLATE.TRUE')
.and.returnValue('Oui');
// création de l'instance en injectant la dépendance via le constructeur
pipe = new TranslateBooleanPipe(translateServiceSpy);
});

it('create an instance', () => {
expect(pipe).toBeTruthy();
});

it(`returns 'Oui' if value is true`, () => {
expect(pipe.transform(true)).toBe('Oui');
});
//...
});

Tests du router

Tests d'un guard (ex: authentication.guard.spec.ts)

 describe('hasRequiredRole', () => {
//1. déclaration des objets du test
let router: Router;
let authServiceSpy: jasmine.SpyObj<AuthService>;

// define routes handled by the router with guards to test
const routes: Routes = [
{
path: 'test',
component: HomePageComponent,
canMatch: [hasRequiredRole],
data: { ROLES: ['ADMIN_TECHNIQUE'] },
},
{
path: '**',
component: ErrorPageComponent,
},
];

//2. using waitForAsync first because compileComponents is async
beforeEach(waitForAsync(() => {
const authServiceSpyObj = jasmine.createSpyObj<AuthService>('AuthService', [
'authenticatedUser$',
]);

void TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes)],
providers: [
{ provide: AuthService, useValue: authServiceSpyObj },
],
}).compileComponents();
}));
//3. then the setup is available
beforeEach(() => {
router = TestBed.inject(Router);
authServiceSpy = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
// use real RedirectService
});

//4. premier test
it('should redirect to login page when no authenticatedUser', (done: DoneFn) => {
authServiceSpy.authenticatedUser$.and.returnValue(of(null));

void router.navigate(['/test']).then(() => {
expect(router.url).toEqual('/login');
done();
});
//5. autres tests se servant de l'initialisation faite aux étapes 1,2,3
//...
});
  1. Au début du test on déclare les objets à tester et les dépendances qui seront mockées (le spyObj). Ici on associe toutes les autres routes au composant ErrorPageComponent par simplicité, on aurait pu l'associer à d'autres composants comme dans les fichiers de routing de l'appli.
  2. Puis dans la première fonction beforeEach on déclare le module en utilisant RouterTestingModule, ce module a besoin d'être compilé, cette compilation est asynchrone, on doit donc déclarer ce beforeEach en premier afin qu'il soit résolu en premier.
  3. On récupère ensuite le router et le mock du service d'auth via l'injecteur d'Angular, TestBed.inject (afin de rester au plus proche du fonctionnement réel).
  4. On peut finalement déclarer un test it et utiliser la navigation du router.