angular-redux/store

How to unit test with redux without TestBed

surfermicha opened this issue · 3 comments

This is a...

  • feature request
  • bug report
  • usage question

What toolchain are you using for transpilation/bundling?

  • @angular/cli
  • Custom @ngTools/webpack
  • Raw ngc
  • SystemJS
  • Rollup
  • Other

Environment

NodeJS Version:
Typescript Version:2.7.2
Angular Version: 5.1.1
@angular-redux/store version: 7.0.0
@angular/cli version: 1.6.1
OS: CentOS (Linux)

Description:

When talking about testing in Angular, many approaches can be found on google, stackoverflow, etc. I followed this instruction here to clearly differ between unit tests and integration tests. Writing unit tests without TestBed works very good until I had to mock an @select property for getting data from the redux store. I cannot directly assign an Observable to the @select property (because it doesn't support setters). I only want to test the pure functions of my components and not the store, services or other dependencies. Therefore I don't use TestBed and instanciate the component object itself instead. For the ng2-redux library (which is a deprecated fork of angular-redux) I found the following issue that has a solution for that. Unfortunately, that is no longer working in angular-redux, because NgRedux is an abstract class now, which cannot be instanciated directly.

#157 references this problem but uses the old ng2-redux library.

Is there any way to mock it without using TestBed?

Abstract your redux code to a separate state service and then use the MockNgRedux module. Here is an example. I've stripped out some business logic and dumbed this down a bit to make it simpler.

import { Injectable } from '@angular/core';
import { select, dispatch } from '@angular-redux/store';
import { Observable } from 'rxjs/Observable';

import { DocumentsArchive } from '../models/documents-archive.interface';
import { DocumentsSelectors } from './documents.selectors';
import { DocumentsActions } from './documents.actions';

@Injectable()
export class DocumentsStateService {

  @select(DocumentsSelectors.documentsArchive) getDocumentsArchive$: Observable<DocumentsArchive[]>;

  @dispatch() onDocumentsArchiveReceipt = DocumentsActions.onDocumentsArchiveReceipt;

}

And to test it:
Pay attention to the code to "getSelectorStub" for mocking @select properties.

import { NgRedux } from '@angular-redux/store';
import { MockNgRedux } from '@angular-redux/store/testing';
import { DocumentsSelectors } from './documents.selectors';
import { DocumentsStateService } from './documents-state.service';
import { DocumentsTypes } from './documents.types';
import { getDocumentsMock } from '../services/documents-archive.mock';

describe('State Service: Documents', () => {
  let mockNgRedux: NgRedux<any>;
  let stateService: DocumentsStateService;

  beforeEach(() => {
    MockNgRedux.reset();
    mockNgRedux = MockNgRedux.getInstance();
    stateService = new DocumentsStateService();
  });

  describe('documents page loading', () => {

    it('should dispatch a documents archive received action', () => {
      const expectedAction = {
        type: DocumentsTypes.DOCUMENTS_ARCHIVE_RECEIVED,
        payload: {
          accountDocuments: [] as any[]
        }
      };
      spyOn(mockNgRedux, 'dispatch');
      stateService.onDocumentsArchiveReceipt({
        accountDocuments: []
      });
      expect(mockNgRedux.dispatch).toHaveBeenCalledWith(expectedAction);
    });

  });

  describe('documents archive', () => {

    it('should get a list of documents', (done: Function) => {
      const documentsArchiveMock = getDocumentsMock().accountDocuments;
      const documentsArchiveStub = MockNgRedux.getSelectorStub(DocumentsSelectors.documentsArchive);
      documentsArchiveStub.next(documentsArchiveMock);
      documentsArchiveStub.complete();

      stateService.getDocumentsArchive$.subscribe(collection => {
        expect(collections[0].documents[0].archiveDate).toEqual('2017-04-04');
        done();
      });
    });
  });

});

I will add that this pattern keeps all of your redux in one place and only requires one place to mock the redux. By injecting the state service into your components, the state service can easily be mocked with jasmine.spy objects for @disptach functions and BehaviorSubjects for the @select properties.

Just test the function you give to the selector in isolation. Testing that @select works is testing library code imho.