CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ngrx--effects

Side effect model for @ngrx/store that manages asynchronous operations and external interactions in Angular applications using RxJS

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

testing.mddocs/

Testing Utilities

Testing support for NgRX Effects with mock providers and utilities that enable comprehensive unit testing of effects without requiring a full store setup.

Capabilities

provideMockActions Function

Creates mock Actions provider for testing effects in isolation, supporting both direct observables and factory functions.

/**
 * Creates mock Actions provider from an observable source
 * @param source - Observable of actions to provide as mock Actions service
 * @returns FactoryProvider for testing setup
 */
function provideMockActions(source: Observable<any>): FactoryProvider;

/**
 * Creates mock Actions provider from a factory function
 * @param factory - Function returning an observable of actions
 * @returns FactoryProvider for testing setup
 */
function provideMockActions(factory: () => Observable<any>): FactoryProvider;

Usage Examples:

import { TestBed } from "@angular/core/testing";
import { provideMockActions } from "@ngrx/effects/testing";
import { Observable, of } from "rxjs";
import { Action } from "@ngrx/store";

describe('BookEffects', () => {
  let actions$: Observable<Action>;
  let effects: BookEffects;
  let bookService: jasmine.SpyObj<BookService>;

  beforeEach(() => {
    const bookServiceSpy = jasmine.createSpyObj('BookService', ['getBooks', 'searchBooks']);

    TestBed.configureTestingModule({
      providers: [
        BookEffects,
        provideMockActions(() => actions$), // Factory function approach
        { provide: BookService, useValue: bookServiceSpy }
      ]
    });

    effects = TestBed.inject(BookEffects);
    bookService = TestBed.inject(BookService) as jasmine.SpyObj<BookService>;
  });

  describe('loadBooks$', () => {
    it('should return loadBooksSuccess action on successful API call', () => {
      const books = [{ id: 1, title: 'Test Book' }];
      const action = BookActions.loadBooks();
      const completion = BookActions.loadBooksSuccess({ books });

      // Arrange
      actions$ = of(action);
      bookService.getBooks.and.returnValue(of(books));

      // Act & Assert
      effects.loadBooks$.subscribe(result => {
        expect(result).toEqual(completion);
      });
    });

    it('should return loadBooksFailure action on API error', () => {
      const error = new Error('API Error');
      const action = BookActions.loadBooks();
      const completion = BookActions.loadBooksFailure({ error: error.message });

      // Arrange
      actions$ = of(action);
      bookService.getBooks.and.returnValue(throwError(error));

      // Act & Assert
      effects.loadBooks$.subscribe(result => {
        expect(result).toEqual(completion);
      });
    });
  });
});

// Alternative setup with direct observable
describe('UserEffects with direct observable', () => {
  let effects: UserEffects;
  let actions$: Observable<Action>;

  beforeEach(() => {
    actions$ = of(UserActions.loadUser({ id: 1 }));

    TestBed.configureTestingModule({
      providers: [
        UserEffects,
        provideMockActions(actions$), // Direct observable approach
        // ... other providers
      ]
    });

    effects = TestBed.inject(UserEffects);
  });

  it('should handle user loading', () => {
    effects.loadUser$.subscribe(action => {
      expect(action.type).toBe('[User] Load User Success');
    });
  });
});

Advanced Testing Patterns

Testing Effects with Multiple Actions:

describe('SearchEffects', () => {
  let actions$: Observable<Action>;
  let effects: SearchEffects;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        SearchEffects,
        provideMockActions(() => actions$),
        // ... mock services
      ]
    });

    effects = TestBed.inject(SearchEffects);
  });

  it('should handle sequence of search actions', () => {
    const actions = [
      SearchActions.searchQuery({ query: 'test' }),
      SearchActions.searchQuery({ query: 'updated' }),
      SearchActions.clearSearch()
    ];

    actions$ = from(actions);

    // Test the effect handles the sequence correctly
    const results: Action[] = [];
    effects.search$.subscribe(action => results.push(action));

    expect(results).toHaveLength(2); // Only search actions, not clear
  });
});

Testing Effects with State Dependencies:

import { MockStore, provideMockStore } from "@ngrx/store/testing";

describe('Effects with Store Dependencies', () => {
  let actions$: Observable<Action>;
  let effects: DataEffects;
  let store: MockStore;

  const initialState = {
    user: { id: 1, name: 'Test User' },
    settings: { theme: 'dark' }
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        DataEffects,
        provideMockActions(() => actions$),
        provideMockStore({ initialState })
      ]
    });

    effects = TestBed.inject(DataEffects);
    store = TestBed.inject(MockStore);
  });

  it('should use store state in effect', () => {
    const action = DataActions.loadUserData();
    actions$ = of(action);

    // Mock selector return value
    store.overrideSelector(selectCurrentUser, { id: 2, name: 'Updated User' });

    effects.loadUserData$.subscribe(result => {
      expect(result).toEqual(
        DataActions.loadUserDataSuccess({
          data: jasmine.objectContaining({ userId: 2 })
        })
      );
    });
  });
});

Testing Non-Dispatching Effects:

describe('Non-dispatching Effects', () => {
  let actions$: Observable<Action>;
  let effects: LoggingEffects;
  let consoleSpy: jasmine.Spy;

  beforeEach(() => {
    consoleSpy = spyOn(console, 'log');

    TestBed.configureTestingModule({
      providers: [
        LoggingEffects,
        provideMockActions(() => actions$)
      ]
    });

    effects = TestBed.inject(LoggingEffects);
  });

  it('should log actions without dispatching', () => {
    const action = AppActions.userLogin({ username: 'testuser' });
    actions$ = of(action);

    // Subscribe to trigger the effect
    effects.logActions$.subscribe();

    expect(consoleSpy).toHaveBeenCalledWith('Action dispatched:', action);
  });
});

Testing Effects with Timing:

import { TestScheduler } from "rxjs/testing";

describe('Effects with Timing', () => {
  let actions$: Observable<Action>;
  let effects: TimingEffects;
  let scheduler: TestScheduler;

  beforeEach(() => {
    scheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });

    TestBed.configureTestingModule({
      providers: [
        TimingEffects,
        provideMockActions(() => actions$)
      ]
    });

    effects = TestBed.inject(TimingEffects);
  });

  it('should debounce search actions', () => {
    scheduler.run(({ hot, cold, expectObservable }) => {
      const action1 = SearchActions.search({ query: 'test' });
      const action2 = SearchActions.search({ query: 'test2' });
      const expected = SearchActions.searchSuccess({ results: [] });

      // Actions with timing
      actions$ = hot('a-b 300ms c', {
        a: action1,
        b: action2,
        c: action2
      });

      // Mock service response
      searchService.search.and.returnValue(cold('100ms (a|)', { a: [] }));

      // Only the last action should trigger the effect after debounce
      expectObservable(effects.debouncedSearch$).toBe(
        '400ms (a|)',
        { a: expected }
      );
    });
  });
});

Testing Functional Effects:

import { runInInjectionContext } from "@angular/core";

describe('Functional Effects', () => {
  let actions$: Observable<Action>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        provideMockActions(() => actions$),
        // ... other providers needed by functional effects
      ]
    });
  });

  it('should test functional effect', () => {
    const action = DataActions.loadData();
    const expectedAction = DataActions.loadDataSuccess({ data: [] });

    actions$ = of(action);

    // Test functional effect in injection context
    runInInjectionContext(TestBed, () => {
      const effect = loadDataEffect();
      
      effect.subscribe(result => {
        expect(result).toEqual(expectedAction);
      });
    });
  });
});

Testing Effects with Error Handling:

describe('Effects Error Handling', () => {
  let actions$: Observable<Action>;
  let effects: ErrorHandlingEffects;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ErrorHandlingEffects,
        provideMockActions(() => actions$),
        // Mock error handler if needed
        { provide: EFFECTS_ERROR_HANDLER, useValue: customErrorHandler }
      ]
    });

    effects = TestBed.inject(ErrorHandlingEffects);
  });

  it('should handle service errors gracefully', () => {
    const action = DataActions.loadData();
    const error = new Error('Service unavailable');
    const expectedAction = DataActions.loadDataFailure({ error: error.message });

    actions$ = of(action);
    dataService.loadData.and.returnValue(throwError(error));

    effects.loadData$.subscribe(
      result => expect(result).toEqual(expectedAction),
      error => fail('Effect should handle errors gracefully')
    );
  });
});

Testing Utilities Type Definitions

interface FactoryProvider {
  provide: any;
  useFactory: Function;
  deps?: any[];
}

interface TestBed {
  configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed;
  inject<T>(token: Type<T> | InjectionToken<T>): T;
}

interface TestModuleMetadata {
  providers?: Provider[];
  imports?: any[];
  declarations?: any[];
}

docs

actions-filtering.md

advanced-features.md

effect-creation.md

index.md

module-setup.md

testing.md

tile.json