or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ajax-operations.mdcombination-operators.mdcore-types.mderror-handling.mdfetch-operations.mdfiltering-operators.mdindex.mdobservable-creation.mdschedulers.mdsubjects.mdtesting-utilities.mdtransformation-operators.mdwebsocket-operations.md

testing-utilities.mddocs/

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

```