Side effect model for @ngrx/store that manages asynchronous operations and external interactions in Angular applications using RxJS
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Testing support for NgRX Effects with mock providers and utilities that enable comprehensive unit testing of effects without requiring a full store setup.
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');
});
});
});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')
);
});
});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[];
}