9

Before lettable operator, I did a helper to modify debounceTime method, so it uses a TestScheduler:

export function mockDebounceTime(
    scheduler: TestScheduler,
    overrideTime: number,
): void {
    const originalDebounce = Observable.prototype.debounceTime;

    spyOn(Observable.prototype, 'debounceTime').and.callFake(function(
        time: number,
    ): void {
        return originalDebounce.call(
            this,
            overrideTime,
            scheduler,
        );
    });
}

So the test of the following Observable was easy:

@Effect()
public filterUpdated$ = this.actions$
    .ofType(UPDATE_FILTERS)
    .debounceTime(DEFAULT_DEBOUNCE_TIME)
    .mergeMap(action => [...])

With lettable operators, the filterUpdated$ Observable is written like that:

@Effect()
public filterUpdated$ = this.actions$
    .ofType(UPDATE_FILTERS)
    .pipe(
        debounceTime(DEFAULT_DEBOUNCE_TIME),
        mergeMap(action => [...])
    );

I cannot patch the debounceTime operator anymore ! How can I pass the testScheduler to the debounceTime operator ?

risingTide
  • 1,754
  • 7
  • 31
  • 60
Guillaume Nury
  • 373
  • 3
  • 7
  • 1
    You can have a look at the [example app form NgRx](https://github.com/ngrx/platform/tree/master/example-app). The scheduler is added using DI in the effects and when testing a different one is provided. – Adrian Fâciu Nov 20 '17 at 07:25
  • However I don't like that approach that much, don't want to add code to effects that is used only for testing and this approach implies the usage of TestBed when testing the effects. When I have some time I'll look for some alternatives. – Adrian Fâciu Nov 20 '17 at 07:26
  • Thx for the link. We should not have to modify our code for testing purpose :( – Guillaume Nury Nov 20 '17 at 10:29
  • As a get arround, I added two methods on my class, so I can spyOn them. But I do not like that solution... – Guillaume Nury Nov 20 '17 at 10:31
  • Came back to this question to write the answer with what I've found and it's pretty much similar to what @cartan wrote below with changing how async scheduler works, since this is what RxJs uses by default. Not that nice but it's the only way I've found that does not involve changing the effect code. – Adrian Fâciu Dec 03 '17 at 20:48

5 Answers5

4

Since .pipe() is still on the Observable prototype, you can use your mocking technique on it.

Lettable operators (oops, now supposed to call them pipeable operators) can be used as-is within the mock pipe.

This is the code I used in app.component.spec.ts of a clean CLI application. Note, it's probably not best use of TestScheduler but shows the principle.

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { Observable } from 'rxjs/Observable';
import { debounceTime, take, tap } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/Rx';

export function mockPipe(...mockArgs) {
  const originalPipe = Observable.prototype.pipe;
  spyOn(Observable.prototype, 'pipe').and.callFake(function(...actualArgs) {
    const args = [...actualArgs];
    mockArgs.forEach((mockArg, index) => {
      if(mockArg) {
        args[index] = mockArg;
      }
    });
    return originalPipe.call(this, ...args);
  });
}

describe('AppComponent', () => {
  it('should test lettable operators', () => {
    const scheduler = new TestScheduler(null);

    // Leave first tap() as-is but mock debounceTime()
    mockPipe(null, debounceTime(300, scheduler));   

    const sut = Observable.timer(0, 300).take(10)
      .pipe(
        tap(x => console.log('before ', x)),
        debounceTime(300),
        tap(x => console.log('after ', x)),
        take(4),
      );
    sut.subscribe((data) => console.log(data));
    scheduler.flush();
  });
});
Richard Matsen
  • 20,671
  • 3
  • 43
  • 77
  • 1
    Best solution imho. However I'd rather patch Observable.prototype.pipe in a way that's agnostic of the exact argument position (so that it just detects that "debounceTime" is given and wraps it accordingly). – NicBright Jun 04 '18 at 13:23
3

You can use the second argument that accepts a custom Scheduler.

  debounceTime(DEFAULT_DEBOUNCE_TIME, rxTestScheduler),

All code

import { Scheduler } from 'rxjs/scheduler/Scheduler';
import { asap } from 'rxjs/scheduler/asap';

@Injectable()
export class EffectsService {
  constructor(private scheduler: Scheduler = asap) { }

  @Effect()
  public filterUpdated$ = this.actions$
    .ofType(UPDATE_FILTERS)
    .pipe(
        debounceTime(DEFAULT_DEBOUNCE_TIME, this.scheduler),
        mergeMap(action => [...])
    );
}

Then on test

describe('Service: EffectsService', () => {
  //setup
  beforeEach(() => TestBed.configureTestingModule({
    EffectsService, 
    { provide: Scheduler, useValue: rxTestScheduler} ]
  }));

  //specs
  it('should update filters using debounce', inject([EffectsService], service => {
    // your test
  });
});
Gerard Sans
  • 2,269
  • 1
  • 13
  • 13
  • Hi, the problem is to inject this custom scheduler only during tests. It was "easy" to mock debounceTime on RxJS 5.4, but now, the only clean solution seems to be way pointed out by @Adrian-Fâciu – Guillaume Nury Dec 01 '17 at 22:05
  • You want your dependencies explicit. This is the way to go. – Gerard Sans Dec 01 '17 at 23:47
  • This worked for me but I had to make sure Scheduler was an available provider somewhere up the chain. `{ provide: Scheduler, useValue: Scheduler }` – metric152 Jan 02 '20 at 17:36
1

If it's difficult to inject or pass the TestScheduler instance to your operators, this simplest solution is to rebind the now and schedule methods of the AsyncScheduler instance to those of the TestScheduler instance.

You could either do this manually:

import { async } from "rxjs/Scheduler/async";

it("should rebind to the test scheduler", () => {

  const testScheduler = new TestScheduler();
  async.now = () => testScheduler.now();
  async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);

  // test something

  delete async.now;
  delete async.schedule;
});

Or you could use a sinon stub:

import { async } from "rxjs/Scheduler/async";
import * as sinon from "sinon";

it("should rebind to the test scheduler", () => {

  const testScheduler = new TestScheduler();
  const stubNow = sinon.stub(async, "now").callsFake(
      () => testScheduler.now()
  );
  const stubSchedule = sinon.stub(async, "schedule").callsFake(
      (work, delay, state) => testScheduler.schedule(work, delay, state)
  );

  // test something

  stubNow.restore();
  stubSchedule.restore();
});
cartant
  • 57,105
  • 17
  • 163
  • 197
  • Interesting workaround, but quite messy in my opinion – Guillaume Nury Dec 01 '17 at 22:06
  • Yeah, I agree, but it's easy enough to wrap up in something reusable: https://github.com/cartant/rxjs-marbles/blob/master/README.md#dealing-with-deeply-nested-schedulers – cartant Dec 01 '17 at 23:40
  • Also, I'm hoping that in RxJS v6 it will be possible to [specify a scheduler at the time of subscription](https://github.com/ReactiveX/rxjs/issues/2935#issuecomment-336681001). Being able to do so would make testing much simpler. – cartant Dec 02 '17 at 08:52
1

Update: should you return an array of actions and you want to verify all of them, remove the

.pipe(throttleTime(1, myScheduler))

And you can use getTestScheduler from jasmine-marbles instead of creating your own scheduler.

import { getTestScheduler } from 'jasmine-marbles';

So a test could look like this:

  it('should pass', () => {
    getTestScheduler().run((helpers) => {
      const action = new fromAppActions.LoadApps();
      const completion1 = new fromAppActions.FetchData();
      const completion2 = new fromAppActions.ShowWelcome();
      actions$ = helpers.hot('-a', { a: action });
      helpers
        .expectObservable(effects.load$)
        .toBe('300ms -(bc)', { b: completion1, c: completion2 });
    });
  });

I was struggling with testing a ngrx effect with debounceTime. It seems things have changed a bit now. I followed the doc here: https://github.com/ReactiveX/rxjs/blob/master/doc/marble-testing.md

Here is what my test looks like:

    describe('someEffect$', () => {
      const myScheduler = new TestScheduler((a, b) => expect(a).toEqual(b));
      it('should test', () => {
        myScheduler.run((helpers) => {
          const action = new fromActions.SomeAction();
          const completion = new fromActions.SomeCompleteAction(someData);
          actions$.stream = helpers.hot('-a', { a: action });

          helpers
            .expectObservable(effects.someEffect$.pipe(throttleTime(1, myScheduler)))
            .toBe('200ms -(b)', { b: completion });
        });
      });
    });

There is no need to use scheduler in the actual code, e.g.: no debounceTime(200, this.scheduler)

techguy2000
  • 4,861
  • 6
  • 32
  • 48
0

I had some problems with the answers above (can't have multiple spies on Observable.prototype, ...), relevant for me was only mocking "debounceTime" so I moved the actual debounceTime (e.g. filterTextDebounceTime = 200) to an variable in the component and in the spec's "beforeEach" I'm setting the component.filterTextDebounceTime to 0 so debounceTime is working synchronously/blocking.

WerthD
  • 51
  • 5
  • (This post does not seem to provide a [quality answer](https://stackoverflow.com/help/how-to-answer) to the question. Please either edit your answer and include the final source code solution, or just post it as a comment to the question). – sɐunıɔןɐqɐp Jun 25 '18 at 07:45