0
# Testing Utilities
1
2
Comprehensive testing framework with marble testing and virtual time scheduling for testing reactive streams and time-dependent operations.
3
4
## Capabilities
5
6
### TestScheduler
7
8
Virtual time scheduler for testing time-dependent operations with marble diagrams.
9
10
```typescript { .api }
11
/**
12
* Test scheduler for marble testing with virtual time control
13
*/
14
class TestScheduler extends VirtualTimeScheduler {
15
/**
16
* Create TestScheduler with assertion function
17
* @param assertDeepEqual - Function to compare actual vs expected results
18
*/
19
constructor(assertDeepEqual: (actual: any, expected: any) => boolean | void);
20
21
/**
22
* Run test with marble testing helpers
23
* @param callback - Test function receiving run helpers
24
* @returns Result from callback
25
*/
26
run<T>(callback: (helpers: RunHelpers) => T): T;
27
28
/**
29
* Create hot observable from marble diagram
30
* @param marbles - Marble diagram string
31
* @param values - Values object mapping marble characters to values
32
* @param error - Error value for error emissions
33
* @returns Hot observable for testing
34
*/
35
createHotObservable<T>(marbles: string, values?: any, error?: any): HotObservable<T>;
36
37
/**
38
* Create cold observable from marble diagram
39
* @param marbles - Marble diagram string
40
* @param values - Values object mapping marble characters to values
41
* @param error - Error value for error emissions
42
* @returns Cold observable for testing
43
*/
44
createColdObservable<T>(marbles: string, values?: any, error?: any): ColdObservable<T>;
45
46
/**
47
* Create expectation for observable
48
* @param observable - Observable to test
49
* @param subscriptionMarbles - Optional subscription timing
50
* @returns Expectation object for assertions
51
*/
52
expectObservable<T>(observable: Observable<T>, subscriptionMarbles?: string): Expectation<T>;
53
54
/**
55
* Create expectation for subscriptions
56
* @param subscriptions - Subscription logs to test
57
* @returns Subscription expectation
58
*/
59
expectSubscriptions(subscriptions: SubscriptionLog[]): SubscriptionExpectation;
60
61
/**
62
* Flush all scheduled work immediately
63
*/
64
flush(): void;
65
66
/**
67
* Get current virtual time frame
68
*/
69
frame: number;
70
71
/**
72
* Maximum number of frames to process
73
*/
74
maxFrames: number;
75
}
76
```
77
78
### RunHelpers Interface
79
80
Helper functions provided by TestScheduler.run().
81
82
```typescript { .api }
83
/**
84
* Helper functions for marble testing
85
*/
86
interface RunHelpers {
87
/**
88
* Create cold observable from marble diagram
89
* @param marbles - Marble diagram string
90
* @param values - Values mapping
91
* @param error - Error value
92
*/
93
cold: <T = string>(marbles: string, values?: any, error?: any) => ColdObservable<T>;
94
95
/**
96
* Create hot observable from marble diagram
97
* @param marbles - Marble diagram string
98
* @param values - Values mapping
99
* @param error - Error value
100
*/
101
hot: <T = string>(marbles: string, values?: any, error?: any) => HotObservable<T>;
102
103
/**
104
* Create expectation for observable behavior
105
* @param observable - Observable to test
106
* @param subscriptionMarbles - Subscription timing
107
*/
108
expectObservable: <T = any>(observable: Observable<T>, subscriptionMarbles?: string) => Expectation<T>;
109
110
/**
111
* Create expectation for subscription behavior
112
* @param subscriptions - Subscription logs
113
*/
114
expectSubscriptions: (subscriptions: SubscriptionLog[]) => SubscriptionExpectation;
115
116
/**
117
* Flush scheduled work
118
*/
119
flush: () => void;
120
121
/**
122
* Get virtual time in frames
123
*/
124
time: (marbles: string) => number;
125
}
126
```
127
128
## Marble Testing Syntax
129
130
### Marble Diagram Characters
131
132
```typescript
133
// Time progression and values
134
'-' // Time passing (1 frame)
135
'a' // Emitted value (can be any character)
136
'|' // Completion
137
'#' // Error
138
'^' // Subscription point
139
'!' // Unsubscription point
140
'(' // Start group (values emitted synchronously)
141
')' // End group
142
143
// Examples:
144
'--a--b--c--|' // Values a, b, c emitted over time, then completion
145
'--a--b--c--#' // Values a, b, c emitted, then error
146
'--a--(bc)--d--|' // Value a, then b and c synchronously, then d, then completion
147
'^--!--' // Subscribe immediately, unsubscribe after 3 frames
148
```
149
150
## Basic Testing Examples
151
152
### Simple Value Testing
153
154
```typescript
155
import { TestScheduler } from "rxjs/testing";
156
import { map, delay } from "rxjs/operators";
157
158
const testScheduler = new TestScheduler((actual, expected) => {
159
expect(actual).toEqual(expected);
160
});
161
162
testScheduler.run(({ cold, hot, expectObservable }) => {
163
// Test map operator
164
const source$ = cold('--a--b--c--|', { a: 1, b: 2, c: 3 });
165
const expected = ' --x--y--z--|';
166
const values = { x: 2, y: 4, z: 6 };
167
168
const result$ = source$.pipe(map(x => x * 2));
169
170
expectObservable(result$).toBe(expected, values);
171
});
172
```
173
174
### Time-based Operator Testing
175
176
```typescript
177
import { TestScheduler } from "rxjs/testing";
178
import { delay, debounceTime, throttleTime } from "rxjs/operators";
179
180
const testScheduler = new TestScheduler((actual, expected) => {
181
expect(actual).toEqual(expected);
182
});
183
184
testScheduler.run(({ cold, expectObservable }) => {
185
// Test delay operator
186
const source$ = cold('--a--b--c--|');
187
const expected = ' ----a--b--c--|';
188
189
const result$ = source$.pipe(delay(20)); // 2 frames delay
190
191
expectObservable(result$).toBe(expected);
192
193
// Test debounceTime
194
const rapidSource$ = cold('--a-b-c----d--|');
195
const debouncedExpected = '----------c----d--|';
196
197
const debounced$ = rapidSource$.pipe(debounceTime(30)); // 3 frames
198
199
expectObservable(debounced$).toBe(debouncedExpected);
200
});
201
```
202
203
### Error Testing
204
205
```typescript
206
import { TestScheduler } from "rxjs/testing";
207
import { map, catchError } from "rxjs/operators";
208
import { of } from "rxjs";
209
210
const testScheduler = new TestScheduler((actual, expected) => {
211
expect(actual).toEqual(expected);
212
});
213
214
testScheduler.run(({ cold, expectObservable }) => {
215
// Test error handling
216
const source$ = cold('--a--b--#', { a: 1, b: 2 }, new Error('test error'));
217
const expected = ' --x--y--(z|)';
218
const values = { x: 2, y: 4, z: 'error handled' };
219
220
const result$ = source$.pipe(
221
map(x => x * 2),
222
catchError(err => of('error handled'))
223
);
224
225
expectObservable(result$).toBe(expected, values);
226
});
227
```
228
229
### Subscription Testing
230
231
```typescript
232
import { TestScheduler } from "rxjs/testing";
233
import { take } from "rxjs/operators";
234
235
const testScheduler = new TestScheduler((actual, expected) => {
236
expect(actual).toEqual(expected);
237
});
238
239
testScheduler.run(({ hot, expectObservable, expectSubscriptions }) => {
240
const source$ = hot('--a--b--c--d--e--|');
241
const sourceSubs = '^----! '; // Subscribe at frame 0, unsubscribe at frame 5
242
const expected = ' --a--b ';
243
244
const result$ = source$.pipe(take(2));
245
246
expectObservable(result$).toBe(expected);
247
expectSubscriptions(source$.subscriptions).toBe(sourceSubs);
248
});
249
```
250
251
## Advanced Testing Patterns
252
253
### Testing Custom Operators
254
255
```typescript
256
import { TestScheduler } from "rxjs/testing";
257
import { OperatorFunction, Observable } from "rxjs";
258
259
// Custom operator to test
260
function skipEveryOther<T>(): OperatorFunction<T, T> {
261
return (source: Observable<T>) => new Observable(subscriber => {
262
let index = 0;
263
return source.subscribe({
264
next(value) {
265
if (index % 2 === 0) {
266
subscriber.next(value);
267
}
268
index++;
269
},
270
error(err) { subscriber.error(err); },
271
complete() { subscriber.complete(); }
272
});
273
});
274
}
275
276
const testScheduler = new TestScheduler((actual, expected) => {
277
expect(actual).toEqual(expected);
278
});
279
280
testScheduler.run(({ cold, expectObservable }) => {
281
const source$ = cold('--a--b--c--d--e--|');
282
const expected = ' --a-----c-----e--|';
283
284
const result$ = source$.pipe(skipEveryOther());
285
286
expectObservable(result$).toBe(expected);
287
});
288
```
289
290
### Testing Async Operations
291
292
```typescript
293
import { TestScheduler } from "rxjs/testing";
294
import { switchMap, catchError } from "rxjs/operators";
295
import { of, throwError } from "rxjs";
296
297
// Mock HTTP service
298
const mockHttpService = {
299
get: (url: string) => {
300
if (url.includes('error')) {
301
return throwError('HTTP Error');
302
}
303
return of({ data: 'success' });
304
}
305
};
306
307
const testScheduler = new TestScheduler((actual, expected) => {
308
expect(actual).toEqual(expected);
309
});
310
311
testScheduler.run(({ cold, expectObservable }) => {
312
// Test successful request
313
const trigger$ = cold('--a--|', { a: '/api/data' });
314
const expected = ' --x--|';
315
const values = { x: { data: 'success' } };
316
317
const result$ = trigger$.pipe(
318
switchMap(url => mockHttpService.get(url))
319
);
320
321
expectObservable(result$).toBe(expected, values);
322
323
// Test error handling
324
const errorTrigger$ = cold('--a--|', { a: '/api/error' });
325
const errorExpected = ' --x--|';
326
const errorValues = { x: 'Request failed' };
327
328
const errorResult$ = errorTrigger$.pipe(
329
switchMap(url => mockHttpService.get(url)),
330
catchError(err => of('Request failed'))
331
);
332
333
expectObservable(errorResult$).toBe(errorExpected, errorValues);
334
});
335
```
336
337
### Testing State Management
338
339
```typescript
340
import { TestScheduler } from "rxjs/testing";
341
import { scan, startWith } from "rxjs/operators";
342
import { Subject } from "rxjs";
343
344
interface AppState {
345
count: number;
346
items: string[];
347
}
348
349
interface AppAction {
350
type: 'increment' | 'add_item';
351
payload?: any;
352
}
353
354
const initialState: AppState = { count: 0, items: [] };
355
356
function reducer(state: AppState, action: AppAction): AppState {
357
switch (action.type) {
358
case 'increment':
359
return { ...state, count: state.count + 1 };
360
case 'add_item':
361
return { ...state, items: [...state.items, action.payload] };
362
default:
363
return state;
364
}
365
}
366
367
const testScheduler = new TestScheduler((actual, expected) => {
368
expect(actual).toEqual(expected);
369
});
370
371
testScheduler.run(({ cold, expectObservable }) => {
372
const actions$ = cold('--a--b--c--|', {
373
a: { type: 'increment' },
374
b: { type: 'add_item', payload: 'item1' },
375
c: { type: 'increment' }
376
});
377
378
const state$ = actions$.pipe(
379
scan(reducer, initialState),
380
startWith(initialState)
381
);
382
383
const expected = 'i--x--y--z--|';
384
const values = {
385
i: { count: 0, items: [] },
386
x: { count: 1, items: [] },
387
y: { count: 1, items: ['item1'] },
388
z: { count: 2, items: ['item1'] }
389
};
390
391
expectObservable(state$).toBe(expected, values);
392
});
393
```
394
395
### Testing Higher-Order Observables
396
397
```typescript
398
import { TestScheduler } from "rxjs/testing";
399
import { mergeMap, switchMap, concatMap } from "rxjs/operators";
400
401
const testScheduler = new TestScheduler((actual, expected) => {
402
expect(actual).toEqual(expected);
403
});
404
405
testScheduler.run(({ cold, hot, expectObservable }) => {
406
// Test mergeMap flattening strategy
407
const source$ = cold('--a----b----c---|');
408
const inner$ = cold( ' x-x-x| ');
409
const expected = ' --x-x-x-x-x-x---|';
410
411
const result$ = source$.pipe(
412
mergeMap(() => inner$)
413
);
414
415
expectObservable(result$).toBe(expected);
416
417
// Test switchMap cancellation
418
const switchSource$ = cold('--a--b--c---|');
419
const switchInner$ = cold( ' x-x-x| ');
420
const switchExpected = ' --x--x--x-x-x|';
421
422
const switchResult$ = switchSource$.pipe(
423
switchMap(() => switchInner$)
424
);
425
426
expectObservable(switchResult$).toBe(switchExpected);
427
});
428
```
429
430
## Test Utilities
431
432
### Helper Functions
433
434
```typescript
435
import { TestScheduler } from "rxjs/testing";
436
import { Observable } from "rxjs";
437
438
// Utility for creating reusable test scheduler
439
function createTestScheduler() {
440
return new TestScheduler((actual, expected) => {
441
expect(actual).toEqual(expected);
442
});
443
}
444
445
// Utility for testing observable emissions
446
export function testObservable<T>(
447
source$: Observable<T>,
448
expectedMarbles: string,
449
expectedValues?: any,
450
expectedError?: any
451
) {
452
const testScheduler = createTestScheduler();
453
454
testScheduler.run(({ expectObservable }) => {
455
expectObservable(source$).toBe(expectedMarbles, expectedValues, expectedError);
456
});
457
}
458
459
// Usage
460
const numbers$ = of(1, 2, 3);
461
testObservable(numbers$, '(abc|)', { a: 1, b: 2, c: 3 });
462
```
463
464
### Testing Complex Scenarios
465
466
```typescript
467
import { TestScheduler } from "rxjs/testing";
468
import { combineLatest, merge } from "rxjs";
469
import { startWith, switchMap } from "rxjs/operators";
470
471
const testScheduler = new TestScheduler((actual, expected) => {
472
expect(actual).toEqual(expected);
473
});
474
475
testScheduler.run(({ cold, hot, expectObservable, time }) => {
476
// Test complex combination of operators
477
const user$ = hot(' --u----U----u--|', { u: 'user1', U: 'user2' });
478
const permissions$ = cold('p--P--| ', { p: ['read'], P: ['read', 'write'] });
479
480
const userWithPermissions$ = combineLatest([user$, permissions$]).pipe(
481
map(([user, perms]) => ({ user, permissions: perms }))
482
);
483
484
const expected = ' --x----y----z--|';
485
const values = {
486
x: { user: 'user1', permissions: ['read'] },
487
y: { user: 'user2', permissions: ['read', 'write'] },
488
z: { user: 'user1', permissions: ['read', 'write'] }
489
};
490
491
expectObservable(userWithPermissions$).toBe(expected, values);
492
493
// Test timing-specific behavior
494
const delayTime = time('---|'); // 3 frames
495
const delayedSource$ = cold('a|').pipe(delay(delayTime));
496
const delayedExpected = ' ---a|';
497
498
expectObservable(delayedSource$).toBe(delayedExpected);
499
});
500
```
501
502
## Types
503
504
```typescript { .api }
505
interface HotObservable<T> extends Observable<T> {
506
subscriptions: SubscriptionLog[];
507
}
508
509
interface ColdObservable<T> extends Observable<T> {
510
subscriptions: SubscriptionLog[];
511
}
512
513
interface SubscriptionLog {
514
subscribedFrame: number;
515
unsubscribedFrame: number;
516
}
517
518
interface Expectation<T> {
519
toBe(marbles: string, values?: any, errorValue?: any): void;
520
toEqual(other: Observable<any>): void;
521
}
522
523
interface SubscriptionExpectation {
524
toBe(marbles: string | string[]): void;
525
}
526
```