Search code examples
angularrxjsjestjsngrxrxjs-marbles

How to test complex ngrx/rxjs service with jest and optionally with marbles


I'm new to javascript testing, and I can't find clear examples to learn how to test these kinds of methods. I clearly need some help here please!

Here are the two methods I'm trying to test :

public _getSearch(search: Search): Observable < any[] > {
    const timeOptions: DateCriteria = this._getTimeStartEnd(search);
    const searchField: string = this._decodeHexIfNecessary(search.searchField.trim());

    return this.store.pipe(
            select(getFilterById(search.filterId)),
            flatMap((filter: Filter) => {

                const indicators = [{
                        'criterias': this.filterService.getActiveCriterias(filter)
                    }
                ];
                search.lastSearch = new TransactionRequest(
                        search.offset, search.pageSize, timeOptions.begin, timeOptions.end,
                        indicators, search.sortData, searchField);

                const request = JSON.stringify(search.lastSearch);
                return this.rest.post(search.urls.get, request, true);
            }));
}

public _countTotalSearch(search: Search): Observable < number > {
    const timeOptions: DateCriteria = this._getTimeStartEnd(search);

    return this.store.pipe(
        select(getFilterById(search.filterId)),
        first(),
        concatMap((filter: Filter) => {
            const indicators = [{
                    'criterias': this.filterService.getActiveCriterias(filter)
                }
            ];
            const request = new TransactionRequest(
                    0, 0, timeOptions.begin, timeOptions.end,
                    indicators, null, search.searchField.trim());

            return this.rest.post(search.urls.count, JSON.stringify(request), true);
        }));
}

I initiated my spec.ts as follows :

    describe('SearchService', () => {
  let searchService: SearchService;
  let store: MockStore;
  let itemsRestService: ItemsRestService;
  let restService: RestService;
  let scheduler: TestScheduler;
  let filterService: FilterService;

  const filterForSelector = TestModelFactory.generateFilter();
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        TranslateModule.forRoot(),
        HttpClientTestingModule
      ],
      providers: [
        SearchService,
        RestService,
        ItemsRestService,
        ReturnCodeRestService,
        {
          provide: FilterService,
          useValue: {}
        },
        {
          provide: DataMapperService,
          useValue: {}
        },
        provideMockStore({
          selectors: [
            {
              selector: 'getFilterById',
              value: filterForSelector
            }
          ]
        })
      ]
    });

    searchService = TestBed.inject(SearchService);
    itemsRestService = TestBed.inject(ItemsRestService);
    restService = TestBed.inject(RestService);
    filterService = TestBed.inject(FilterService);
    scheduler = new TestScheduler(((actual, expected) => {
      expect(actual).toEqual(expected);
    }));
    store = TestBed.inject(MockStore);
  });
});

I tried a lot of things, like subscribing, or marbles... But I can't make it work. I'd like to check the result (observableFromApi) and to check the post request as well :

  describe('getSearch', () => {
it('should select filters in store and call api', done => {
  // init vars
  const dateCriteria = new DateCriteria(faker.date.recent().getTime(), faker.date.recent().getTime());
  const resultFromApi = faker.random.word() as any;
  const observableFromApi = of(resultFromApi);
  const search = TestModelFactory.generateSearch();

  // spy
  spyOn(restService, 'post');
  spyOn(searchService, '_getTimeStartEnd');

  // mocks
  filterService.getActiveCriterias = jest.fn(() => faker.random.word() as any);
  searchService._getTimeStartEnd = jest.fn(() => dateCriteria);
  restService.post = jest.fn(() => observableFromApi);

  // call and expect
  expect(searchService._getSearch(search)).toEqual(observableFromApi);
  expect(restService.post).toHaveBeenCalledWith(search.urls.get, '', true);
});
  });

Can you help me please?

EDIT :

This way it starts to work, but I have an error :

  describe('getSearch', () => {
it('should select filters in store and call api', done => {
  // init vars
  const dateCriteria = new DateCriteria(faker.date.recent().getTime(), faker.date.recent().getTime());
  const resultFromApi = faker.random.word() as any;
  const observableFromApi = of(resultFromApi);
  const search = TestModelFactory.generateSearch();

  // spy
  spyOn(restService, 'post');
  spyOn(searchService, '_getTimeStartEnd');

  // mocks
  filterService.getActiveCriterias = jest.fn(() => faker.random.word() as any);
  searchService._getTimeStartEnd = jest.fn(() => dateCriteria);
  // const expectedObservable = cold('a|', {a : observableFromApi});
  // restService.post = jest.fn(() => expectedObservable);

  restService.post = jest.fn(() => observableFromApi);

  searchService._getSearch(search).subscribe(res => {
    done();
    expect(res).toBe(observableFromApi);
    expect(restService.post).toHaveBeenCalledWith(search.urls.get, '', true);
  });
});
  });

Here is the error, I think the selector is not mocked properly, the flatMap is not called, I added a tap before the selector and it was called.

  console.error
    Error: Uncaught [TypeError: Cannot read property 'ids' of undefined]

EDIT 2 :

I have the following error when I try to mock my selector :

enter image description here

This is how I declared the selector, what is wrong here please ?

export const getFilterById = (id) => createSelector(selectAll, (allItems: Filter[]) => {
  if (allItems) {
    return allItems.find((item: Filter) => {
      return item.id === id;
    });
  } else {
    return null;
  }
});

Solution

  • I found a solution by extracting the selector in a method :

      public _getSearch(search: Search): Observable<any[]> {
        const timeOptions: DateCriteria = this._getTimeStartEnd(search);
        const searchField: string = this._decodeHexIfNecessary(search.searchField.trim());
    
        const filterObservable: Observable<Filter> = this._getFilterById(search);
    
        const result = filterObservable.map((filter: Filter) => {
          // Préparation de la requête
          const indicators = [{ 'criterias': this.filterService.getActiveCriterias(filter) }];
          search.lastSearch = new TransactionRequest(
            search.offset, search.pageSize, timeOptions.begin, timeOptions.end,
            indicators, search.sortData, searchField
          );
    
          const request = JSON.stringify(search.lastSearch);
          return this.rest.post(search.urls.get, request, true);
        });
        return result as Observable<any>;
      }
      
      public _getFilterById(search: Search) {
        return this.store.select(getFilterById(search.filterId));
      }
    

    And then I mocked this method in my tests :

    searchService._getFilterById = jest.fn(() => of(filterForSelector));