cartant / rxjs-marbles

An RxJS marble testing library for any test framework

Home Page:https://cartant.github.io/rxjs-marbles/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Testing expectations after flush

wwjhu opened this issue · comments

If I would like to test a number of expectations after the flush has occurred, what would be the advised approach? Right now I'm disabling autoFlush and call flush manually, but that adds a bit of plumbing to every test that needs it.

If you always want to set autoFlush to false, you could wrap and re-export the marbles function from a module of your own, like this:

import { marbles as _marbles } from "rxjs-marbles";

export function marbles(func: (m: Context, ...rest: any[]) => any): any {

    return _marbles((m: Context, ...rest: any[]) => {
        m.autoFlush = false;
        return func(m, ...rest);
    });
}

That's the approach taken within rxjs-marbles for framework-specific configuration.

That would work although I would look at extending the Context class. Would that be something that you consider as interesting as feature for rxjs-marbles?

it('should ...', marbles((m: Context) => {

    // ...
	
    m.onFlush(() => {
        // Some extra tests here
    })
}));

I'm hesitant to do that. You could always add it to the Context using the wrapper approach.

I'm curious as to why you would need to perform post-flush tests so often. What do your tests look like?

The observable chain does calls to methods, in my test case a mocked api call. At the end of the test I want to verify the number of calls made to this api. That is something that can only be checked after the flush.

So those methods are side-effects and their being called cannot otherwise be verified? I.e. via results returned via the observable chain?

Correct, the chain in this case includes an exhaustMap, filter and first

  /**
   * Polls an API at an interval of x milliseconds for a period of y milliseconds. Ensures that only a single poll is
   * active at a given point. The interval is therefore a minimum interval: if at the next interval a poll is in
   * progress, the poll is skipped for that interval point.
   *
   * The returned observable emits the first value that passes the provided filter and completes afterwards. If within
   * period no values pass the filter, the observable errors with a TimeoutError
   *
   * Inspired by https://stackoverflow.com/questions/48212752/rxjs-periodic-polling-of-an-endpoint-with-a-variable-response-time#answer-48218063
   *
   * @param retryInterval interval in milliseconds
   * @param retryTimeout timeout in milliseconds
   * @param apiFn callback to function providing the observables to poll
   * @param filterFn callback to filter function to signal success
   * @param scheduler used for testing
   * @return {Observable<T>}
   */
  static poll<T>(retryInterval: number, retryTimeout: number, apiFn: () => Observable<T>, filterFn: (r: T) => boolean, scheduler?: IScheduler) : Observable<T> {
    const timeoutAt = new Date((scheduler ? scheduler.now() : Date.now()) + retryTimeout);
    return Observable
      .timer(0, retryInterval) // Try to call the api at every interval
      .exhaustMap(() => apiFn()) // But avoid multiple calls to the api at the same time
      .filter((r: T) => filterFn(r)) // Check whether the caller accepts the value
      .first() // We're done when the first item gets through the filter
      .timeout(timeoutAt); // Or we're done when we run out of time
  }

I'm still hesitant to add it. I kinda prefer flush to be something you either don't deal with because you write conventional marble tests or something that's completely explicit because you are doing something a little different.

I'm not sure, but I have vague memories of writing tests in which I called flush multiple times, so onFlush just seems a little weird to me, at the moment.

I'll give it some more thought, but in the interim, you can always add it yourself using the wrapper mechanism:

import { marbles as _marbles } from "rxjs-marbles";

declare module "rxjs-marbles/context" {
    interface Context {
        onFlush?: () => void;
    }
}

export function marbles(func: (m: Context, ...rest: any[]) => any): any {
    return _marbles((m: Context, ...rest: any[]) => {
        const result = func(m, ...rest);
        if (m.onFlush) { m.onFlush(); }
        return result;
    });
}

TypeScript's declaration merging makes this sort of thing possible.

I'm still hesitant to add it. I kinda prefer flush to be something you either don't deal with because you write conventional marble tests or something that's completely explicit because you are doing something a little different.

Understood.

Thanks for the snippet and the excellent library.

No worries. There were some errors in that last snippet. They should be fixed, now.