Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test observable containing a debounce operator?

Tags:

rxjs

jasmine

How does one write a Jasmine test to test an observable with the debounce operator? I've followed this blog post and understand the principles of how it should be tested, but it just doesn't seem to work.

Below is the factory that I am using to create the observable:

import Rx from "rx/dist/rx.all";
import DOMFactory from "../utils/dom-factory";
import usernameService from "./username.service";

function createUsernameComponent(config) {
  const element = DOMFactory(config);

  const username = Rx.Observable
    .fromEvent(element.find('input'), 'input')
    .pluck('target', 'value')
    .startWith(config.value);

  const isAvailable = username
    .debounce(500)
    .tap(() => console.info('I am never called!'))
    .flatMapLatest(usernameService.isAvailable)
    .startWith(false);

  const usernameStream = Rx.Observable.combineLatest(username, isAvailable)
    .map((results) => {
      const [username, isAvailable] = results;
      return isAvailable ? username : ''
    })
    .distinctUntilChanged();

  return Object.freeze({
    stream: usernameStream,
    view: element
  });
}

export default createUsernameComponent;

Note that tap operator is never called by the test. However, it will be executed properly if I run this code on the browser.

Below is my attempt at the test:

import Rx from "rx/dist/rx.all";
import Username from "./username.component";
import DataItemBuilder from "../../../test/js/utils/c+j-builders";
import usernameService from "./username.service"

describe('Username Component', () => {
  let input, username;

  beforeEach(() => {
    const usernameConfig = DataItemBuilder.withName('foo')
      .withPrompt('label').withType('text').build();

    const usernameComponent = Username(usernameConfig);
    usernameComponent.stream.subscribe(value => username = value);

    input = usernameComponent.view.find('input');
  });

  it('should set to a valid username after debounce', () => {
    const scheduler = injectTestSchedulerIntoDebounce();
    scheduler.scheduleRelative(null, 1000, () => {
      doKeyUpTest('abcddd', 'abcdd');
      scheduler.stop();
    });
    scheduler.start();
    scheduler.advanceTo(1000);
  });

  function injectTestSchedulerIntoDebounce() {
    const originalOperator = Rx.Observable.prototype.debounce;
    const scheduler = new Rx.TestScheduler();

    spyOn(Rx.Observable.prototype, 'debounce').and.callFake((dueTime) => {
      console.info('The mocked debounce is never called!');
      if (typeof dueTime === 'number') {
        return originalOperator.call(this, dueTime, scheduler);
      }
      return originalOperator.call(this, dueTime);
    });

    return scheduler;
  }

  function doKeyUpTest(inputValue, expectation) {
    input.val(inputValue);
    input.trigger('input');
    expect(username).toBe(expectation);
  }
});

When I run the test, the fake debounce never gets called. I plan to mock the username service once I can get past the debounce.

like image 957
Pete Avatar asked May 25 '16 04:05

Pete


2 Answers

In your test code you are triggering the input event inside the scheduleRelative function. This doesn't work because you are advancing 1000ms before doing the change. The debouncer then waits 500ms to debounce the isAvailable call but you already stopped the scheduler so time is not advancing afterwards.

What you should do is: trigger the input event before advancing the scheduler time or even better in a scheduleRelative function for a time <= 500ms in a and then inside the scheduleRelative function for 1000ms you have to call the expect function with the expected output and then stop the scheduler.

It should look like this:

  it('should set to a valid username after debounce', () => {
    const scheduler = injectTestSchedulerIntoDebounce();

    scheduler.scheduleRelative(null, 500, () => {
      input.val(inputValue);
      input.trigger('input');
    });

    scheduler.scheduleRelative(null, 1000, () => {
      expect(username).toBe(expectation);
      scheduler.stop();
    });

    scheduler.start();
    scheduler.advanceTo(1000);
  });

In addition to that I have better experience with scheduleAbsolute instead of scheduleRelative because it is less confusing.

like image 138
Simon Jentsch Avatar answered Nov 09 '22 07:11

Simon Jentsch


As per Simon Jentsch's answer, below is the answer using scheduleAbsolute instead of scheduleRelative:

import Rx from "rx/dist/rx.all";
import Username from "./username.component";
import DataItemBuilder from "../../../test/js/utils/c+j-builders";
import usernameService from "./username.service"

describe('Username Component', () => {
  let input, username, promiseHelper;

  const scheduler = new Rx.TestScheduler(0);

  beforeEach(() => {
    spyOn(usernameService, 'isAvailable').and.callFake(() => {
      return Rx.Observable.just(true);
    });
  });

  beforeEach(() => {
    const usernameConfig = DataItemBuilder.withName('foo')
      .withPrompt('label').withType('text').build();

    const usernameComponent = Username(usernameConfig, scheduler);
    usernameComponent.stream.subscribe(value => username = value);

    input = usernameComponent.view.find('input');
  });

  it('should set the username for valid input after debounce', (done) => {
    doKeyUpTest('abcddd', '');
    scheduler.scheduleAbsolute(null, 100, () => {
      expect(usernameService.isAvailable).not.toHaveBeenCalled();
      expect(username).toBe('');
    });
    scheduler.scheduleAbsolute(null, 1000, () => {
      expect(usernameService.isAvailable).toHaveBeenCalled();
      expect(username).toBe('abcddd');
      scheduler.stop();
      done();
    });
    scheduler.start();
  });

  function doKeyUpTest(inputValue, expectation) {
    input.val(inputValue);
    input.trigger('input');
    expect(username).toBe(expectation);
  }

});
like image 37
Pete Avatar answered Nov 09 '22 07:11

Pete