web-dev-qa-db-fra.com

Comment tester un effet NGRX qui utilise un sélecteur de magasin?

J'ai un @Effect Qui utilise un MemoizedSelector pour récupérer un élément du magasin redux et mergeMap avec la charge utile d'une action. L'effet fonctionne très bien, mais la configuration des tests Jest pour cela s'est avérée difficile car je n'arrive pas à me moquer de la valeur de retour du sélecteur car select est une fonction déclarée qui est importée (de '@ ngrx/store' ) et utilisé dans l'effet et le sélecteur lui-même étant également une fonction importée. Je saisis les pailles maintenant.

Comment puis-je écrire un test unitaire pour tester un effet NGRX qui utilise un sélecteur de magasin?
"@ ngrx/store": "^ 7.4.0" ,
"rxjs": "^ 6.2.2"

J'ai essayé les types de solutions suivants:

  1. en utilisant
provideMockStore({
  initialState
})

provideMockStore provient de '@ngrx/store/testing'; où l'état initial était à la fois mon réel initialState et un état qui contient la structure/l'élément exact que j'essaie de sélectionner

  1. en utilisant différents types de MockStore à partir de diverses SO questions/réponses ainsi que différentes approches de billets de blog

  2. tenter de se moquer du sélecteur en utilisant <selector>.projector(<my-mock-object>) (saisissant la paille ici, je suis presque sûr que cela serait utilisé dans des tests isolés du sélecteur pas de l'effet)

L'effet lui-même:

@Effect()
  getReviewsSuccess$ = this.actions$.pipe(
    ofType<ProductActions.GetReviewsSuccess>(
      ProductActions.ProductActionTypes.GET_REVIEWS_SUCCESS
    ),
    mergeMap(() => this.reduxStore.pipe(select(selectProduct))),
    map(({ product_id }) => product_id),
    map(product_id => new ProductActions.GetReviewsMeta({
      product_id,
    }))
  );

La spécification:

......
  let effects: ProductEffects;
  let facade: Facade;
  let actions$: Observable<any>;
  let store$: Observable<State>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule,
        // ^ I've also tried using StoreModule.forRoot(...) here to configure 
        // it in similar fashion to the module where this effect lives
      ],
      providers: [
        ProductEffects,
        provideMockActions(() => actions$),
        {
          provide: Facade,
          useValue: facadeServiceMock,
        },
        ResponseService,
        provideMockStore({
          initialState
        })
        // ^ also tried setting up the test with different variations of initialState
      ],
    });
......

it('should return a GetReviewsMeta on successful GetReviewsSuccess', () => {
    const reviews = {...reviewListMock};
    const { product_id } = {...productMockFull};

    const action = new ProductActions.GetReviewsSuccess({
      reviews
    });

    const outcome = new ProductActions.GetReviewsMeta({
      product_id
    });

    actions$ = hot('-a', { a: action }); 

    // store$ = cold('-c', { c: product_id });  
    // not sure what, if anything I need to do here to mock select(selectProduct)  

    const expected = cold('-b', { b: outcome });  
    expect(effects.getReviewsSuccess$).toBeObservable(expected);
  });

Le sélecteur selectProduct:

export const getProduct = ({product}: fromProducts.State) => product;

export const getProductState = createFeatureSelector<
    fromProducts.State
>('product');

export const selectProduct = createSelector(
  getProductState,
  getProduct,
);

Je m'attends à ce que le test réussisse, mais à la place, je reçois toujours l'erreur suivante

● Product Effects › should return a GetReviewsMeta on successful GetReviewsSuccess

    expect(received).toBeNotifications(expected)

    Expected notifications to be:
      [{"frame": 10, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": {"product_id": 2521}, "type": "[Reviews] Get Reviews Meta"}}}]
    But got:
      [{"frame": 10, "notification": {"error": [TypeError: Cannot read property 'product_id' of undefined], "hasValue": false, "kind": "E", "value": undefined}}]

De toute évidence, le MemoizedSelector (selectProduct) ne sait pas quel est l'objet produit qui devrait être dans le magasin (mais il ne semble pas que j'injecte un initialState qui l'ait ou non) et ne peut pas obtenir le product_id du produit car je ne l'ai pas configuré correctement dans le beforeEach ou dans la spécification elle-même ...

8
Andrew B

Nous avons couvert cela dans les documents ngrx.io . Notez que la syntaxe est pour NgRx 8 mais les mêmes idées comptent pour NgRx 7.

addBookToCollectionSuccess$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(CollectionApiActions.addBookSuccess),
        withLatestFrom(this.store.pipe(select(fromBooks.getCollectionBookIds))),
        tap(([, bookCollection]) => {
          if (bookCollection.length === 1) {
            window.alert('Congrats on adding your first book!');
          } else {
            window.alert('You have added book number ' + bookCollection.length);
          }
        })
      ),
    { dispatch: false }
  );
it('should alert number of books after adding the second book', () => {
      store.setState({
        books: {
          collection: {
            loaded: true,
            loading: false,
            ids: ['1', '2'],
          },
        },
      } as fromBooks.State);

      const action = CollectionApiActions.addBookSuccess({ book: book1 });
      const expected = cold('-c', { c: action });
      actions$ = hot('-a', { a: action });
      expect(effects.addBookToCollectionSuccess$).toBeObservable(expected);
      expect(window.alert).toHaveBeenCalledWith('You have added book number 2');
    });
  });

Assurez-vous que votre état a la même structure que dans les devtools redux.

NgRx 8 fournit également un moyen de se moquer des sélecteurs, il n'est donc pas nécessaire de configurer l'arborescence d'état entière pour un seul test - https://next.ngrx.io/guide/store/testing#using- simulateurs .

describe('Auth Guard', () => {
  let guard: AuthGuard;
  let store: MockStore<fromAuth.State>;
  let loggedIn: MemoizedSelector<fromAuth.State, boolean>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [AuthGuard, provideMockStore()],
    });

    store = TestBed.get(Store);
    guard = TestBed.get(AuthGuard);

    loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false);
  });

  it('should return false if the user state is not logged in', () => {
    const expected = cold('(a|)', { a: false });

    expect(guard.canActivate()).toBeObservable(expected);
  });

  it('should return true if the user state is logged in', () => {
    const expected = cold('(a|)', { a: true });

    loggedIn.setResult(true);

    expect(guard.canActivate()).toBeObservable(expected);
  });
});
4
timdeschryver