Dans un composant, nous utilisons un sélecteur ngrx pour récupérer différentes parties de l'état.
public isListLoading$ = this.store.select(fromStore.getLoading);
public users$ = this.store.select(fromStore.getUsers);
le fromStore.method
est créé à l'aide de la méthode ngrx createSelector
. Par exemple:
export const getState = createFeatureSelector<UsersState>('users');
export const getLoading = createSelector(
getState,
(state: UsersState) => state.loading
);
J'utilise ces observables dans le modèle:
<div class="loader" *ngIf="isLoading$ | async"></div>
<ul class="userList">
<li class="userItem" *ngFor="let user of $users | async">{{user.name}}</li>
</div>
Je voudrais écrire un test où je pourrais faire quelque chose comme:
store.select.and.returnValue(someSubject)
pour pouvoir modifier la valeur du sujet et tester le modèle du composant contre ces valeurs.
Le fait est que nous luttons pour trouver un moyen approprié de tester cela. Comment écrire ma méthode "andReturn" puisque la méthode select
est appelée deux fois dans mon composant, avec deux méthodes différentes (MemoizedSelector) comme arguments?
Nous ne souhaitons pas utiliser de sélecteur réel et par conséquent, se moquer d'un état puis utiliser le sélecteur réel ne semble pas être une méthode de test unitaire appropriée (les tests ne seraient pas isolés et utiliseraient des méthodes réelles pour tester le comportement d'un composant).
J'ai rencontré le même défi et je l'ai résolu une fois pour toutes en intégrant mes sélecteurs dans des services. Ainsi, mes composants ont simplement utilisé le service pour obtenir leurs données plutôt que de passer directement par le magasin. J'ai trouvé ceci nettoyé mon code, rendu mes tests indépendants de l'implémentation et rendu beaucoup plus facile de se moquer:
mockUserService = {
get users$() { return of(mockUsers); },
get otherUserRelatedData$() { return of(otherMockData); }
}
TestBed.configureTestingModule({
providers: [{ provide: UserService, useValue: mockUserService }]
});
Avant de faire cela cependant, je devais résoudre le problème dans votre question.
La solution dépendra de l’endroit où vous enregistrez les données. Si vous enregistrez dans la constructor
comme:
constructor(private store: Store) {
this.users$ = store.select(getUsers);
}
Ensuite, vous devrez recréer le composant de test chaque fois que vous souhaitez modifier la valeur renvoyée par la variable store
. Pour ce faire, créez une fonction dans ce sens:
const createComponent = (): MyComponent => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
return component;
};
Et appelez ensuite cela après avoir modifié la valeur de ce que votre magasin espion renvoie:
describe('test', () => {
it('should get users from the store', () => {
const users: User[] = [{username: 'BlackHoleGalaxy'}];
store.select.and.returnValue(of(users));
const cmp = createComponent();
// proceed with assertions
});
});
Sinon, si vous définissez la valeur dans ngOnInit
:
constructor(private store: Store) {}
ngOnInit() {
this.users$ = this.store.select(getUsers);
}
Les choses sont un peu plus faciles, car vous pouvez créer le composant une fois et rappeler simplement ngOnInit
à chaque fois que vous souhaitez modifier la valeur de retour du magasin:
describe('test', () => {
it('should get users from the store', () => {
const users: User[] = [{username: 'BlackHoleGalaxy'}];
store.select.and.returnValue(of(users));
component.ngOnInit();
// proceed with assertions
});
});
J'ai également rencontré ce problème et utiliser des services pour envelopper les sélecteurs n'est pas une option pour moi aussi. Surtout pas seulement à des fins de test et parce que j'utilise le magasin pour remplacer des services.
C'est pourquoi j'ai proposé la solution suivante (qui n'est pas parfaite):
J'utilise un 'Store' différent pour chaque composant et chaque aspect. Dans votre exemple, je définirais les magasins et états suivants:
export class UserStore extends Store<UserState> {}
export class LoadingStatusStore extends Store<LoadingStatusState> {}
Et les injecter dans le composant utilisateur:
constructor( private userStore: UserStore, private LoadingStatusStore:
LoadingStatusStore ) {}
Mock-les à l'intérieur de la classe User-Component-Test-Class:
TestBed.configureTestingModule({
imports: [...],
declarations: [...],
providers: [
{ provide: UserStore, useClass: MockStore },
{ provide: LoadingStatusStore, useClass: MockStore }
]
}).compileComponents();
Injectez-les dans la méthode de test beforeEach () ou it ():
beforeEach(
inject(
[UserStore, LoadingStatusStore],
(
userStore: MockStore<UserState>,
loadingStatusStore: MockStore<LoadingStatusState>
) => {...}
Ensuite, vous pouvez les utiliser pour espionner les différentes méthodes de canalisation:
const userPipeSpy = spyOn(userStore, 'pipe').and.returnValue(of(user));
const loadingStatusPipeSpy = spyOn(loadingStatusStore, 'pipe')
.and.returnValue(of(false));
L'inconvénient de cette méthode est que vous ne pouvez toujours pas tester plus d'une partie d'un état d'un magasin dans une méthode de test. Mais pour l'instant, cela fonctionne comme une solution de contournement pour moi.