the use of the RxJS delay operator within Ava tests

240 views Asked by At

I'm the author of a JavaScript program which needs to be executed against a number of tests. I discovered that each of these tests follow a similar format and as such it's appropriate to generate the tests using a factory function.

The factory function is called testFactory and is shown in the code below. It is invoked with a data array which, once my application logic is added, will instruct the factory on how to build a particular test. The factory function is responsible for executing the tests as well.

I selected RxJS in order to achieve a two second delay if a 'delay' string is included in the data passed to the factory. (My previous attempt at an implementation used promises, but I failed at getting this approach working.) As you can see, this is accomplished though the use of the Rx delay operator.

Ava was selected because it has a reputation of working with RxJS. As you can see, I'm making calls to an Rx Subject from a subscription to my observable.

In my actual application code, this subscription makes calls to a state machine which implements my application logic and the data from the state machine is fed into the subject through calls to the next method of the subject in a callback method on the state machine. This is why I'm not able to simply plug my observable directly into the Ava test method, but instead have to go through a subject. A subject was chosen instead of an observable for the ability of a subject to allow calls to be made to its next and complete methods from outside its definition.

I have removed my application logic from the code below so as not to confuse the issue with those details. The problem comes when the 'delay' string is removed from the data array. When this code is run with data that does not contain a delay, the test does not pass:

const data = [
  { r: 'c' },
  { l: 'c' },
  { l: 'n' },
  { l: 'c' }
];

It fails with: Test finished without running any assertions.

How do I get this passing when there is no 'delay' in the data array? Why does this fail when there is no 'delay' in the data array? Thank you.

const ava = require('ava');
const { test } = ava;

const Rx = require('rxjs/Rx');
const { Observable, Subject } = Rx;

const data = [
  { r: 'c' },
  { l: 'c' },
  { l: 'n' },
  'delay',
  { l: 'c' }
];

const testFactory = (data) => {
  let subject = new Subject();

  // This code adds a delay property to each item which passes through, adding a
  // delay value based on a cumulative delay value maintained by the scan
  // operator. Items which are simply delays are marked with a 'skip' property
  // in the scan and skipped in the flatMap. Once the delay has been performed
  // by the delay operator, the added delay property is deleted. If there is a
  // simpler way in Rx to achieve this functionality, I'm open to suggestions
  // on refactoring this code. :-)
  const source = Observable.from(data)
    .scan((acc, val) => {
      if (val === 'delay') {
        return { skip: true, delay: acc.delay + 2000 };
      }
      return Object.assign(val, { delay: acc.delay });
    }, { delay: 0 })
    .flatMap((e) => {
      if (e.skip) {
        return Observable.empty();
      } else {
        return Observable.of(e)
          .delay(e.delay)
          .map(e => { delete e.delay; return e; });
      }
    });

  // This is the subscribe block which in my application called my state
  // machine. Since the state machine has been removed, the Subject is called
  // directly, instead of calling it from the callback tot the state machine.
  // Either way, the same problem exists. 
  source
    .subscribe({
      next: e => {
        subject.next(e);
      },
      complete: () => {
        subject.complete();
      }
    });

  // This test always passes. When the 'delay' is removed, the failure would
  // indicate to me that its never called.  
  test('', t => {
    // t.plan(1);
    return subject
      .map((n) => {
        t.true(true);
      });
  });
};

testFactory(data);

Note: Interestingly, when the import of Ava is removed along with the line below which imports Ava's test function, and the call to the test function is replaced with a regular RxJS subscribe on the subject, the code works both with and without the 'delay' string in the data structure:

  // test('', t => {
  //   // t.plan(1);
  //   return subject
  //     .map((n) => {
  //       t.true(true);
  //     });
  // });

  subject.subscribe((v) => {
    console.log(JSON.stringify(v));
  });

Does this indicate the problem is with my use of Ava?

1

There are 1 answers

0
Mark Wubben On

I'm not terribly familiar with RxJS, observables or subjects, but there's a clue in Test finished without running any assertions.

AVA tests can be synchronous, or asynchronous when you return an observable, promise, or use test.cb(t => t.end()). AVA also fails tests if no assertions were run before the test finishes.

In your case, it looks like AVA has determined your test is synchronous. You should make sure it's asynchronous, and only end it when the data is fully consumed.