angular-redux / store

Angular 2+ bindings for Redux
MIT License
1.34k stars 202 forks source link

How to unit test with redux without TestBed #517

Open surfermicha opened 6 years ago

surfermicha commented 6 years ago

This is a...

What toolchain are you using for transpilation/bundling?

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?

gregkopp commented 6 years ago

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();
      });
    });
  });

});
gregkopp commented 6 years ago

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.

DcsMarcRemolt commented 6 years ago

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