Marble Testing

Preview — 3 of 10 questions

What is the Observable contract (Rx Contract) that every Observable must honor?

javascript
// This is the valid emission sequence any Observable must follow:
// next → next → next → (complete | error)  ← then NOTHING more

// Implementing a compliant Observable:
const compliant$ = new Observable<number>(subscriber => {
  // May call next any number of times
  subscriber.next(1);
  subscriber.next(2);

  // Must call exactly one of these:
  subscriber.complete();
  // OR: subscriber.error(new Error('oops'));

  // These calls are IGNORED after complete/error (SafeSubscriber guards this):
  subscriber.next(3);       // ignored
  subscriber.complete();    // ignored
  subscriber.error('late'); // ignored
});

// RxJS's SafeSubscriber wraps user observers to enforce the contract:
// It sets an internal 'isStopped' flag after error/complete
// and ignores subsequent notifications
compliant$.subscribe({
  next: v => console.log(v),       // 1, 2
  complete: () => console.log('done'), // "done"
  // 3 is never logged — contract is enforced by SafeSubscriber
});
AAn Observable may emit any number of values in any order, including after completion or error
BAn Observable must always emit at least one value before completing
CAn Observable must call next* zero or more times, followed by exactly one error OR one complete — never both, and no emissions after either terminal event
DAn Observable can only emit synchronously — async emissions require Subject

How does TestScheduler achieve time-travel in virtual time tests?

javascript
import { TestScheduler } from 'rxjs/testing';

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

scheduler.run(({ cold, hot, expectObservable, expectSubscriptions }) => {
  // Virtual time test — runs synchronously, no real waiting
  const source$ = hot('  -a-b-c-d-e-|');
  const sub1 =         '  ^----!       ';  // subscribe at 0, unsub at 50ms
  const sub2 =         '       ^----!  ';  // subscribe at 60ms, unsub at 110ms
  const expected1 =    '  -a-b-        ';
  const expected2 =    '       -d-e-  ';

  expectObservable(source$, sub1).toBe(expected1);
  expectObservable(source$, sub2).toBe(expected2);
});

// Testing switchMap behavior:
scheduler.run(({ cold, hot, expectObservable }) => {
  const source$ = hot('  -a--b-----c-|  ');
  const inner$  = cold(' --x-y-|        ');
  //                              ↑ inner for each outer value

  const result = source$.pipe(
    switchMap(() => inner$),
  );

  // When 'b' arrives, the inner Observable from 'a' is cancelled:
  expectObservable(result).toBe('  ---x--x-y-x-y-|');
});
AIt patches Date.now() and setTimeout globally during test execution
BIt runs the test in a Web Worker that runs at 1000x speed
CIt maintains a virtual clock and a priority queue of scheduled actions; flush() advances the clock synchronously, processing all scheduled work without real waiting
DIt uses JavaScript's performance.now() with a custom offset

When should you use effect() instead of toSignal(obs$.pipe(tap(...)))?

javascript
@Component({ standalone: true, changeDetection: ChangeDetectionStrategy.OnPush })
export class DashboardComponent {
  private userService = inject(UserService);
  private analyticsService = inject(AnalyticsService);

  // toSignal — binds Observable data to a signal for the template
  // ✅ Use for: displaying data, computed values
  users = toSignal(this.userService.getUsers(), { initialValue: [] });
  activeCount = computed(() => this.users().filter(u => u.active).length);

  // effect — reacts to signal changes with side effects
  // ✅ Use for: logging, analytics, DOM mutations, calling other services
  constructor() {
    effect(() => {
      // Runs whenever activeCount() changes
      this.analyticsService.track('active_users', { count: this.activeCount() });
    });
  }
}

// Contrast: toSignal for template binding
// ✅ Value available in template synchronously
template = `<p>Active: {{ activeCount() }}</p>`;

// Anti-pattern: using effect for data transformation
// ❌ DON'T do this — use computed() instead
effect(() => {
  this.doubleCount = this.count() * 2;  // write to signal in effect is a warning
});
AUse effect() for side effects that should react to signal changes (logging, analytics, DOM updates); use toSignal() for binding Observable values to the template
BAlways use effect() — toSignal() doesn't support side effects
Ceffect() runs synchronously; toSignal() runs asynchronously — choose based on timing needs
DThere is no difference — effect() is built on toSignal() internally

Sign up free to play

Answer all 10 questions (7 more), see explanations for every answer, and track your score.