@maklja90/chaijs-rxjs-observable-spy
TypeScript icon, indicating that this package has built-in type declarations

1.2.3 • Public • Published

@maklja90/chaijs-rxjs-observable-spy

ChaiJS extension for testing RxJS observables

npm version release codecov code style: prettier License: MIT

Installation

yarn add -D @maklja90/chaijs-rxjs-observable-spy

or

npm install -D @maklja90/chaijs-rxjs-observable-spy

What problem this library is trying to solve?

To read more about the problem this library solves visit the following link.
This library extends the ChaiJS language with support for testing RxJS observables.

Usage

Typescript

This library is fully covered with types and most of the methods can accept generics in order to define what values will be received or what error is expected to be thrown. This library also packs types that extend the ChaiJS language chain. Library contains source maps, so it is possible to debug a library from tests.

Browsers

Library transpile to ES5 and should work in all browsers that support that version of javascript.

Setup ChaiJS plugin

Before using the library features it is required to register a plugin with a ChaiJS.

import chai from 'chai';
import { createChaiObservableSpyPlugin } from '@maklja90/chaijs-rxjs-observable-spy';

chai.use(createChaiObservableSpyPlugin());

Using CommonJS:

const chai = require('chai');
const {
  createChaiObservableSpyPlugin,
} = require('@maklja90/chaijs-rxjs-observable-spy');

If you are using the mocha test framework with NodeJS, you could use the register function to set up a plugin before tests start execution.

mocha --require @maklja90/chaijs-rxjs-observable-spy/register ./src/**/*.spec.js

Language use cases

Library offers an emit, observableSpy or oSpy keywords to indicate that the value that is tested is observable and to access the rest of language chains.
observableSpy, or short oSpy, is a function that can receive a name of the tested observable. This name will be printed out in the error messages that occur in tests and this can be helpful when debugging tests. Alternatively if this is not important to you you can use property emit instead.

it('should allow observableSpy to accept name argument', async () => {
  const strings$ = of('Tom', 'Tina', 'Ana');

  // will fail with an error that has observable name in the message
  // [next(stringsObservable)] - expected next value: John, actual value Tina
  await expect(strings$)
    // short name is oSpy
    .observableSpy('stringsObservable')
    .next('Tom')
    .next('John') // expected values is 'John', but value 'Tina' is received
    .next('Ana')
    .verifyComplete();
});

next, nextCount, nextMatches and nextMatchesUntil keywords

Keyword next indicates the next value that will be received from observable, note that order here matters. Keyword next is doing deep equals of received value and expected value.

it('should receive ordered strings with the complete event', async () => {
  const strings$ = of('Tom', 'Tina', 'Ana');

  // verifyComplete will return Promise that will resolve with
  // received values from observable
  const values = await expect(strings$)
    // alternative to 'emit' is to use word 'observableSpy'
    .emit.next('Tom')
    .next('Tina')
    .next('Ana')
    // start a verification and
    // expect to observable ends with complete event
    // alternative is to call complete and then verify
    .verifyComplete();

  expect(values).to.deep.equals(['Tom', 'Tina', 'Ana']);
});

Keyword nextCount should be used to count values without actually checking their value. Note that this keyword expects to receive an complete event and because of that we are using verify instead of verifyComplete keyword.

it('should receive proper values count', async () => {
  const strings$ = of('Tom', 'Ana');

  // expect to receive 2 values and then the complete event
  const values = await expect(strings$).emit.nextCount(2).verify();
  expect(values).to.deep.equals(['Tom', 'Ana']);
});

Keyword nextMatches should be used to create a validation condition for value that will yield a true or false, depending if the value is valid or not. This keyword is useful if you are using a third party assertion library or custom made assertion functions.

it('should next value match a condition', async () => {
  const numbers$ = of(2, 2, 3);

  // assert that verifies a value
  // if false is returned the assertion inside a library will throw an error
  const customAssertion = (val: number): boolean => val > 1;

  const values = await expect(numbers$)
    .emit.nextMatches(customAssertion)
    .nextMatches(customAssertion)
    .nextMatches(customAssertion)
    .verifyComplete();

  expect(values).to.deep.equals([2, 2, 3]);
});

Keyword nextMatchesUntil is similar to the nextMatches keyword, the difference is only that this keyword has an additional condition that determines when the current verification step is over.

it('should next value match a condition until', async () => {
  const sourceNumbers = [2, 2, 3];
  const numbers$ = from(sourceNumbers);

  // assert that verifies a value
  // if false is returned the assertion inside a library will throw an error
  const conditionMatch = (val: number): boolean => val > 1;

  const values = await expect(numbers$)
    .emit.nextMatchesUntil<number>(
      conditionMatch,
      // condition that indicates if the current verification step is over
      // when a false is returned, the library will proceed
      // with the next verification step.
      (_, index) => index < sourceNumbers.length - 1,
    )
    .verifyComplete();

  expect(values).to.deep.equals([2, 2, 3]);
});

consumeNext and consumeNextUntil keywords

Keyword consumeNext can be used to just "consume" a value and if required to do required operations on received values.

it('should consume next value', async () => {
  const numbers$ = of(2, 2, 3);

  // if you are using typescript you can specify generic types '<number>'
  const values = await expect(numbers$)
    .emit.consumeNext<number>((val) =>
      expect(val).to.be.a('number').and.to.be.equal(2),
    )
    .consumeNext<number>((val) =>
      expect(val).to.be.a('number').and.to.be.equal(2),
    )
    .consumeNext<number>((val) =>
      expect(val).to.be.a('number').and.to.be.equal(3),
    )
    .verifyComplete();

  expect(values).to.deep.equals([2, 2, 3]);
});

Similar to the keyword consumeNext the keyword consumeNextUntil will verify values until callback does not return falsy value.

it('should consume next value until satisfies condition', async () => {
  const sourceValues = [2, 2, 3];
  const numbers$ = from(sourceValues);

  const values = await expect(numbers$)
    .emit.consumeNextUntil<number>((val, index) => {
      expect(val)
        .to.be.a('number')
        .and.to.be.equal(sourceValues[index]);
      // if true is returned the verification step is still not over,
      // otherwise the next verification step will start with execution
      return index < sourceValues.length - 1;
    })
    .verifyComplete();

  expect(values).to.deep.equals([2, 2, 3]);
});

skipCount and skipUntil keyword

Keyword skipCount can be used to skip N next values.

it('should skip values', async () => {
  const strings$ = of('Tom', 'Ana', 'John');

  const values = await expect(strings$)
    .emit.skipCount(2) // skip next 2 values
    .next('John')
    .verifyComplete();

  expect(values).to.deep.equals(['Tom', 'Ana', 'John']);
});

Keyword skipUntil can be used to skip N next values by some condition.

it('should skip values until condition', async () => {
  const strings$ = of('Tom', 'Ana', 'John');

  const values = await expect(strings$)
    .emit.skipUntil((_, index) => index < 1) // skip while index < 1
    .next('John')
    .verifyComplete();

  expect(values).to.deep.equals(['Tom', 'Ana', 'John']);
});

awaitSingle keyword

Should be used when a single result is expected from the tested observable.

it('should receive a single next value and complete', async () => {
  const string$ = of('John');

  // result will be a single value instead of array of values
  const singleString = await expect(
    string$,
  ).emit.awaitSingle<string>();
  expect(singleString).to.be.equal('John');
});

error, errorType, errorMessage and consumeError keywords

Keywords error, errorType, errorMessage and consumeError should be used when handling errors from observables. Note that here we are using only the keyword verify to indicate that verification should start, but that complete event is not expected to be received from the tested observable.

it('should catch an error from observable', async () => {
  const error$ = throwError(() => new Error('Unexpected error'));

  await expect(error$).emit.error(Error, 'Unexpected error').verify();
  await expect(error$).emit.errorType(Error).verify();
  await expect(error$).emit.errorMessage('Unexpected error').verify();
  await expect(error$)
    .emit.consumeError<Error>((e) => {
      expect(e).to.be.instanceOf(Error);
      expect(e.message).to.be.equal('Unexpected error');
    })
    .verify();
});

observable keyword

Check if the tested object is an instance of observable.

it('should not throw an error when instance of Observable', function () {
  expect(of(1, 2, 3)).to.be.an.observable();
});

virtualTime keyword

Setup a flag for the library to use TestScheduler from 'rxjs/testing'. This is useful when we want to use virtual time instead of real time. Useful when using interval, delay...

it('should use virtual time and receive values in proper order', async function () {
  const interval$ = interval(1_000).pipe(
    filter((val) => val % 2 === 1),
    take(2),
  );

  const values = await expect(interval$)
    .emit.virtualTime.next(1)
    .next(3)
    .verifyComplete();
  expect(values).to.deep.equals([1, 3]);
});

verify, complete, verifyComplete and awaitComplete keywords

Keyword verify is always used after keywords error, errorType, errorMessage or complete and its purpose is to subscribe observable spy to the tested observable and start testing received values or errors.
Keyword verifyComplete is used as a shortcut of two keywords complete and verify and indicates that the observable should end up with a complete event and that verification of the observable should start.
Keyword awaitComplete will just wait for a complete event from the observable and ignore all next values received from it. Useful when it is required to just verify that an observable ends with a completed event without bothering about the values that are sent from it.

it('should just await complete event and ignore next values', async () => {
  const strings$ = of('Tom', 'Ana', 'John');

  // 'then' keyword has no functionality,
  // it is there just for readability :)
  const values = await expect(strings$).emit.then.awaitComplete();
  expect(values).to.deep.equals(['Tom', 'Ana', 'John']);
});

awaitComplete can receive an callback to process next values.

it('should just await complete event and assert next values', async () => {
  const sourceValues = ['Tom', 'Ana', 'John'];
  const strings$ = from(sourceValues);

  const values = await expect(strings$).emit.then.awaitComplete(
    (val, index) => expect(val).to.be.equal(sourceValues[index]),
  );
  expect(values).to.deep.equals(['Tom', 'Ana', 'John']);
});

Forgot to call verify keyword

In case we forgot to call the verify keyword the tests will look like they are passing. The reason for this is simple: without a call to the verify the observable spy does not subscribe to the tested observable and in that case no values are received and so there is nothing to test.
Library has a safe guard that will throw an error if some language chain ends without proper verify keyword.

it('verify is not call at the end', async () => {
  const strings$ = from(['Tom', 'Ana']);

  // there is no call to verify or verifyComplete
  // because of this spy will not be subscribed
  // to tested strings$ observable, as result the error will be thrown
  // [observableSpyGuard]: found not subscribed observable in test
  await expect(strings$).emit.next('Tom').next('Tina').next('Ana');
});

Under a hood the library awaits 2 seconds for a test to call verify keyword, if in that time window the verify method is not called the error is thrown. It is possible to configure timeout when creating the plugin, also it is possible to to disable this behavior if required.

import chai from 'chai';
import { createChaiObservableSpyPlugin } from '@maklja90/chaijs-rxjs-observable-spy';

chai.use(
  // pass configuration to the plugin
  createChaiObservableSpyPlugin({
    forgottenSubscriptionError: true,
    forgottenSubscriptionTimeout: 2_000, // ms
  }),
);

ChaiJS should assertion type

This library also extends ChaiJS's should assertion style of writing tests.

import chai from 'chai';
import { createChaiObservableSpyPlugin } from '@maklja90/chaijs-rxjs-observable-spy';
// other RxJS imports

chai.should();

chai.use(createChaiObservableSpyPlugin());

it('should receive values in proper order with complete event', async () => {
  const strings$ = from(['Tom', 'Tina', 'Ana']);

  const values = await strings$.should.emit
    .next('Tom')
    .next('Tina')
    .next('Ana')
    .verifyComplete<string>();

  values.should.be.deep.equals(['Tom', 'Tina', 'Ana']);
});

License

MIT

Package Sidebar

Install

npm i @maklja90/chaijs-rxjs-observable-spy

Weekly Downloads

0

Version

1.2.3

License

MIT

Unpacked Size

78.5 kB

Total Files

87

Last publish

Collaborators

  • maklja90