Search code examples
angularunit-testingjestjsngxs

NGXS Actions Observable Jest Expectation


Im currently facing a strange Behavior. When I want to run multiple expectations on the store state after successful finishing the Store Action I'm unable to observe the reason of test failure.

If all expectations are met, the test runs successfully (see console logs below). In case of a failing assertion the done callback is never called and the error of the expectation is not thrown to the test runner. (see console log below - timeout).

As a reference test I created a Subject and called it with next. Everything works as expected! It seems to be an issue with the Actions from '@ngxs/store'.

Is there any known issue? Am I using the Actions provider in a wrong way?

Details of my setup:

➜ npx envinfo --system --npmPackages                                 

  System:
    OS: macOS 12.0.1
    CPU: (12) x64 Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz
    Memory: 3.02 GB / 32.00 GB
    Shell: 5.8 - /bin/zsh
  npmPackages:
    @angular-devkit/build-angular: ~12.2.1 => 12.2.1 
    @angular-devkit/build-webpack: ^0.1202.1 => 0.1202.1 
    @angular-eslint/builder: ~12.2.1 => 12.2.2 
    @angular-eslint/eslint-plugin: ~12.2.1 => 12.2.2 
    @angular-eslint/eslint-plugin-template: ~12.2.1 => 12.2.2 
    @angular-eslint/schematics: ~12.6.1 => 12.6.1 
    @angular-eslint/template-parser: ~12.2.1 => 12.2.2 
    @angular/animations: ~12.2.1 => 12.2.1 
    @angular/cdk: ~12.2.1 => 12.2.1 
    @angular/cli: ~12.2.1 => 12.2.1 
    @angular/common: ~12.2.1 => 12.2.1 
    @angular/compiler: ~12.2.1 => 12.2.1 
    @angular/compiler-cli: ~12.2.1 => 12.2.1 
    @angular/core: ~12.2.1 => 12.2.1 
    @angular/flex-layout: ^12.0.0-beta.34 => 12.0.0-beta.34 
    @angular/forms: ~12.2.1 => 12.2.1 
    @angular/localize: ^12.2.1 => 12.2.1 
    @angular/platform-browser: ~12.2.1 => 12.2.1 
    @angular/platform-browser-dynamic: ~12.2.1 => 12.2.1 
    @angular/router: ~12.2.1 => 12.2.1 
    @ngxs/store: ^3.7.3 => 3.7.3 
    @types/crypto-js: ^4.0.2 => 4.0.2 
    @types/jest: ^27.0.3 => 27.0.3 
    @types/node: ^12.11.1 => 12.20.15 
    @typescript-eslint/eslint-plugin: ^4.23.0 => 4.23.0 
    @typescript-eslint/parser: ^4.23.0 => 4.23.0 
    eslint: ^7.26.0 => 7.29.0 
    eslint-config-prettier: ^8.3.0 => 8.3.0 
    eslint-plugin-prettier: ^3.4.0 => 3.4.0 
    husky: ^7.0.4 => 7.0.4 
    jest: ^27.4.3 => 27.4.3 
    jest-preset-angular: ^11.0.1 => 11.0.1 
    prettier: ^2.3.2 => 2.3.2 
    prettier-eslint: ^12.0.0 => 12.0.0 
    rxjs: ~6.6.0 => 6.6.7 
    tslib: ^2.1.0 => 2.3.0 
    typescript: ~4.2.3 => 4.2.4 
    zone.js: ~0.11.4 => 0.11.4 

Code example:

import { TestBed } from '@angular/core/testing';
import { Actions, NgxsModule, ofActionCompleted, Store } from '@ngxs/store';

describe('Review Task Store', () => {
  let store: Store;
  let actions$: Actions;

  beforeEach(() => {
    TestBed.configureTestingModule({ imports: [NgxsModule.forRoot([TestState])] });
    store = TestBed.inject(Store);
    actions$ = TestBed.inject(Actions);
  });

  it('should example', (done) => {
    // Arrange
    actions$
      .pipe(ofActionCompleted(ExampleAction))
      .subscribe(() => {
        // Assert
        console.log('i was here! Before');
        expect(true).toBeFalsy();
        console.log('i was here! After');
        done();
      });

    // Act
    store.dispatch(new ExampleAction());
  });
});

Happy-case logs:

console.log
    i was here! Before
console.log
    i was here! After

Unhappy-case logs:

  console.log
    i was here! Before


: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error: 
[...]

Solution

  • One possible solution is to move the assertions to a step of the pipeline and add a catchError to show all errors contained.

    But it generates some boilerplate code :/

     it('should show expectation error', (done) => {
            // Arrange
            const assertFn = (actionUnderTest) => {
                // Assert
                console.log('ACTION$ -> i was here! Before');
                expect(true).toBeFalsy();
                console.log('ACTION$ -> i was here! After');
                done();
            }
            actions$
                .pipe(
                    ofActionCompleted(ExampleAction),
                    tap(assertFn),
                    catchError((err, caught) => {
                        done.fail(err)
                        return caught;
                    })
                )
                .subscribe();
            // Act
            store.dispatch(new ExampleAction(testValue));
        });