Scott DePouw
Scott DePouw

Reputation: 3899

NgRx Effects Unit Testing: Test catchError Case, Without Marbles

I'm trying to test a simple scenario, with the following Effect defined:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { TodosService } from '../services/todos.service';
import { getTodos, getTodosSuccess } from './todos.actions';

@Injectable()
export class TodosEffects {
  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(getTodos),
      mergeMap(() => this.todosService.getTodos()
        .pipe(
          map(todos => getTodosSuccess({ todoItems: todos })),
          catchError(() => EMPTY)
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private todosService: TodosService
  ) { }
}

In this scenario, I am deliberately trying to get this working without Marbles. That is, without hot() cold() and so on. The few examples I've found online covering this topic just use those, but I'm looking for a way to do this without that for now.

Here's what I have in my test class so far. The mocking of TodosService, first test of the happy path, mock actions setup, etc. all work as expected. The second test, the one where I'm basically trying to confirm that the EMPTY observable is generated by catchError, is where I'm struggling:

import { TestBed } from '@angular/core/testing';
import { TodosService } from '@app/services/todos.service';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action } from '@ngrx/store';
import { EMPTY, Observable, of } from 'rxjs';
import { TodoItem } from './todo-model';
import { getTodos, getTodosSuccess } from './todos.actions';
import { TodosEffects } from './todos.effects';

let actions$ = new Observable<Action>();

let effects: TodosEffects;
let todosServiceSpy: jasmine.SpyObj<TodosService>;

describe('Todos Effects', () => {
  beforeEach(() => {
    const todosServiceSpyObject = jasmine.createSpyObj('TodosService', ['getTodos']);

    TestBed.configureTestingModule({
      providers: [
        TodosEffects,
        provideMockActions(() => actions$),
        { provide: TodosService, useValue: todosServiceSpyObject }
      ]
    });

    effects = TestBed.inject(TodosEffects);
    todosServiceSpy = TestBed.inject<TodosService>(TodosService) as jasmine.SpyObj<TodosService>;
  });

  it('should return successful todo get action with service results', () => {
    actions$ = of(getTodos());
    const expectedTodos: TodoItem[] = [{} as TodoItem];
    todosServiceSpy.getTodos.and.returnValue(of(expectedTodos));
    const expectedAction = getTodosSuccess({ todoItems: expectedTodos });

    effects.loadTodos$.subscribe(result => {
      expect(result).toEqual(expectedAction);
    });
  });

  it('should return EMPTY observable I think?', () => {
    actions$ = of(getTodos());
    // todosServiceSpy.getTodos.and.throwError('');
    todosServiceSpy.getTodos.and.returnValue(throwError(''));
    const expected = EMPTY;

    effects.loadTodos$.subscribe();
    // TODO: What to expect / etc. here?
  });
});

The NgRx Effects testing documentation is unfortunately quite lacking here, as it only posts snippets and edge case testing scenarios for examples.

Upvotes: 0

Views: 1320

Answers (1)

Drenai
Drenai

Reputation: 12377

Since EMPTY completes without emitting next or error, you could use that as the test

let nextCount = 0
let errorCount = 0
let completeCount = 0

effects.loadTodos$.subscribe(
  () =>  {nextCount++},
  () =>  {errorCount++}
  () =>  {completeCount++}
)

// test that only completeCount === 1, and others are 0
expect(....).toEqual(...)

Upvotes: 1

Related Questions