0
# Testing Utilities
1
2
Testing support for NgRX Effects with mock providers and utilities that enable comprehensive unit testing of effects without requiring a full store setup.
3
4
## Capabilities
5
6
### provideMockActions Function
7
8
Creates mock Actions provider for testing effects in isolation, supporting both direct observables and factory functions.
9
10
```typescript { .api }
11
/**
12
* Creates mock Actions provider from an observable source
13
* @param source - Observable of actions to provide as mock Actions service
14
* @returns FactoryProvider for testing setup
15
*/
16
function provideMockActions(source: Observable<any>): FactoryProvider;
17
18
/**
19
* Creates mock Actions provider from a factory function
20
* @param factory - Function returning an observable of actions
21
* @returns FactoryProvider for testing setup
22
*/
23
function provideMockActions(factory: () => Observable<any>): FactoryProvider;
24
```
25
26
**Usage Examples:**
27
28
```typescript
29
import { TestBed } from "@angular/core/testing";
30
import { provideMockActions } from "@ngrx/effects/testing";
31
import { Observable, of } from "rxjs";
32
import { Action } from "@ngrx/store";
33
34
describe('BookEffects', () => {
35
let actions$: Observable<Action>;
36
let effects: BookEffects;
37
let bookService: jasmine.SpyObj<BookService>;
38
39
beforeEach(() => {
40
const bookServiceSpy = jasmine.createSpyObj('BookService', ['getBooks', 'searchBooks']);
41
42
TestBed.configureTestingModule({
43
providers: [
44
BookEffects,
45
provideMockActions(() => actions$), // Factory function approach
46
{ provide: BookService, useValue: bookServiceSpy }
47
]
48
});
49
50
effects = TestBed.inject(BookEffects);
51
bookService = TestBed.inject(BookService) as jasmine.SpyObj<BookService>;
52
});
53
54
describe('loadBooks$', () => {
55
it('should return loadBooksSuccess action on successful API call', () => {
56
const books = [{ id: 1, title: 'Test Book' }];
57
const action = BookActions.loadBooks();
58
const completion = BookActions.loadBooksSuccess({ books });
59
60
// Arrange
61
actions$ = of(action);
62
bookService.getBooks.and.returnValue(of(books));
63
64
// Act & Assert
65
effects.loadBooks$.subscribe(result => {
66
expect(result).toEqual(completion);
67
});
68
});
69
70
it('should return loadBooksFailure action on API error', () => {
71
const error = new Error('API Error');
72
const action = BookActions.loadBooks();
73
const completion = BookActions.loadBooksFailure({ error: error.message });
74
75
// Arrange
76
actions$ = of(action);
77
bookService.getBooks.and.returnValue(throwError(error));
78
79
// Act & Assert
80
effects.loadBooks$.subscribe(result => {
81
expect(result).toEqual(completion);
82
});
83
});
84
});
85
});
86
87
// Alternative setup with direct observable
88
describe('UserEffects with direct observable', () => {
89
let effects: UserEffects;
90
let actions$: Observable<Action>;
91
92
beforeEach(() => {
93
actions$ = of(UserActions.loadUser({ id: 1 }));
94
95
TestBed.configureTestingModule({
96
providers: [
97
UserEffects,
98
provideMockActions(actions$), // Direct observable approach
99
// ... other providers
100
]
101
});
102
103
effects = TestBed.inject(UserEffects);
104
});
105
106
it('should handle user loading', () => {
107
effects.loadUser$.subscribe(action => {
108
expect(action.type).toBe('[User] Load User Success');
109
});
110
});
111
});
112
```
113
114
### Advanced Testing Patterns
115
116
**Testing Effects with Multiple Actions:**
117
118
```typescript
119
describe('SearchEffects', () => {
120
let actions$: Observable<Action>;
121
let effects: SearchEffects;
122
123
beforeEach(() => {
124
TestBed.configureTestingModule({
125
providers: [
126
SearchEffects,
127
provideMockActions(() => actions$),
128
// ... mock services
129
]
130
});
131
132
effects = TestBed.inject(SearchEffects);
133
});
134
135
it('should handle sequence of search actions', () => {
136
const actions = [
137
SearchActions.searchQuery({ query: 'test' }),
138
SearchActions.searchQuery({ query: 'updated' }),
139
SearchActions.clearSearch()
140
];
141
142
actions$ = from(actions);
143
144
// Test the effect handles the sequence correctly
145
const results: Action[] = [];
146
effects.search$.subscribe(action => results.push(action));
147
148
expect(results).toHaveLength(2); // Only search actions, not clear
149
});
150
});
151
```
152
153
**Testing Effects with State Dependencies:**
154
155
```typescript
156
import { MockStore, provideMockStore } from "@ngrx/store/testing";
157
158
describe('Effects with Store Dependencies', () => {
159
let actions$: Observable<Action>;
160
let effects: DataEffects;
161
let store: MockStore;
162
163
const initialState = {
164
user: { id: 1, name: 'Test User' },
165
settings: { theme: 'dark' }
166
};
167
168
beforeEach(() => {
169
TestBed.configureTestingModule({
170
providers: [
171
DataEffects,
172
provideMockActions(() => actions$),
173
provideMockStore({ initialState })
174
]
175
});
176
177
effects = TestBed.inject(DataEffects);
178
store = TestBed.inject(MockStore);
179
});
180
181
it('should use store state in effect', () => {
182
const action = DataActions.loadUserData();
183
actions$ = of(action);
184
185
// Mock selector return value
186
store.overrideSelector(selectCurrentUser, { id: 2, name: 'Updated User' });
187
188
effects.loadUserData$.subscribe(result => {
189
expect(result).toEqual(
190
DataActions.loadUserDataSuccess({
191
data: jasmine.objectContaining({ userId: 2 })
192
})
193
);
194
});
195
});
196
});
197
```
198
199
**Testing Non-Dispatching Effects:**
200
201
```typescript
202
describe('Non-dispatching Effects', () => {
203
let actions$: Observable<Action>;
204
let effects: LoggingEffects;
205
let consoleSpy: jasmine.Spy;
206
207
beforeEach(() => {
208
consoleSpy = spyOn(console, 'log');
209
210
TestBed.configureTestingModule({
211
providers: [
212
LoggingEffects,
213
provideMockActions(() => actions$)
214
]
215
});
216
217
effects = TestBed.inject(LoggingEffects);
218
});
219
220
it('should log actions without dispatching', () => {
221
const action = AppActions.userLogin({ username: 'testuser' });
222
actions$ = of(action);
223
224
// Subscribe to trigger the effect
225
effects.logActions$.subscribe();
226
227
expect(consoleSpy).toHaveBeenCalledWith('Action dispatched:', action);
228
});
229
});
230
```
231
232
**Testing Effects with Timing:**
233
234
```typescript
235
import { TestScheduler } from "rxjs/testing";
236
237
describe('Effects with Timing', () => {
238
let actions$: Observable<Action>;
239
let effects: TimingEffects;
240
let scheduler: TestScheduler;
241
242
beforeEach(() => {
243
scheduler = new TestScheduler((actual, expected) => {
244
expect(actual).toEqual(expected);
245
});
246
247
TestBed.configureTestingModule({
248
providers: [
249
TimingEffects,
250
provideMockActions(() => actions$)
251
]
252
});
253
254
effects = TestBed.inject(TimingEffects);
255
});
256
257
it('should debounce search actions', () => {
258
scheduler.run(({ hot, cold, expectObservable }) => {
259
const action1 = SearchActions.search({ query: 'test' });
260
const action2 = SearchActions.search({ query: 'test2' });
261
const expected = SearchActions.searchSuccess({ results: [] });
262
263
// Actions with timing
264
actions$ = hot('a-b 300ms c', {
265
a: action1,
266
b: action2,
267
c: action2
268
});
269
270
// Mock service response
271
searchService.search.and.returnValue(cold('100ms (a|)', { a: [] }));
272
273
// Only the last action should trigger the effect after debounce
274
expectObservable(effects.debouncedSearch$).toBe(
275
'400ms (a|)',
276
{ a: expected }
277
);
278
});
279
});
280
});
281
```
282
283
**Testing Functional Effects:**
284
285
```typescript
286
import { runInInjectionContext } from "@angular/core";
287
288
describe('Functional Effects', () => {
289
let actions$: Observable<Action>;
290
291
beforeEach(() => {
292
TestBed.configureTestingModule({
293
providers: [
294
provideMockActions(() => actions$),
295
// ... other providers needed by functional effects
296
]
297
});
298
});
299
300
it('should test functional effect', () => {
301
const action = DataActions.loadData();
302
const expectedAction = DataActions.loadDataSuccess({ data: [] });
303
304
actions$ = of(action);
305
306
// Test functional effect in injection context
307
runInInjectionContext(TestBed, () => {
308
const effect = loadDataEffect();
309
310
effect.subscribe(result => {
311
expect(result).toEqual(expectedAction);
312
});
313
});
314
});
315
});
316
```
317
318
**Testing Effects with Error Handling:**
319
320
```typescript
321
describe('Effects Error Handling', () => {
322
let actions$: Observable<Action>;
323
let effects: ErrorHandlingEffects;
324
325
beforeEach(() => {
326
TestBed.configureTestingModule({
327
providers: [
328
ErrorHandlingEffects,
329
provideMockActions(() => actions$),
330
// Mock error handler if needed
331
{ provide: EFFECTS_ERROR_HANDLER, useValue: customErrorHandler }
332
]
333
});
334
335
effects = TestBed.inject(ErrorHandlingEffects);
336
});
337
338
it('should handle service errors gracefully', () => {
339
const action = DataActions.loadData();
340
const error = new Error('Service unavailable');
341
const expectedAction = DataActions.loadDataFailure({ error: error.message });
342
343
actions$ = of(action);
344
dataService.loadData.and.returnValue(throwError(error));
345
346
effects.loadData$.subscribe(
347
result => expect(result).toEqual(expectedAction),
348
error => fail('Effect should handle errors gracefully')
349
);
350
});
351
});
352
```
353
354
### Testing Utilities Type Definitions
355
356
```typescript { .api }
357
interface FactoryProvider {
358
provide: any;
359
useFactory: Function;
360
deps?: any[];
361
}
362
363
interface TestBed {
364
configureTestingModule(moduleDef: TestModuleMetadata): typeof TestBed;
365
inject<T>(token: Type<T> | InjectionToken<T>): T;
366
}
367
368
interface TestModuleMetadata {
369
providers?: Provider[];
370
imports?: any[];
371
declarations?: any[];
372
}
373
```