Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kwonoj/rx-sandbox
Marble diagram DSL based test suite for RxJS
https://github.com/kwonoj/rx-sandbox
marble observable rxjs test typescript
Last synced: about 1 month ago
JSON representation
Marble diagram DSL based test suite for RxJS
- Host: GitHub
- URL: https://github.com/kwonoj/rx-sandbox
- Owner: kwonoj
- License: mit
- Created: 2017-04-14T05:19:00.000Z (over 7 years ago)
- Default Branch: master
- Last Pushed: 2023-10-20T13:11:47.000Z (about 1 year ago)
- Last Synced: 2024-10-19T18:30:17.497Z (about 2 months ago)
- Topics: marble, observable, rxjs, test, typescript
- Language: TypeScript
- Homepage:
- Size: 2.74 MB
- Stars: 172
- Watchers: 4
- Forks: 11
- Open Issues: 12
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-angular - rx-sandbox - Marble diagram DSL based test suite for RxJS. (Table of contents / Third Party Components)
- fucking-awesome-angular - rx-sandbox - Marble diagram DSL based test suite for RxJS. (Table of contents / Third Party Components)
README
[![Build Status](https://circleci.com/gh/kwonoj/rx-sandbox/tree/master.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/kwonoj/rx-sandbox/tree/master)
[![codecov](https://codecov.io/gh/kwonoj/rx-sandbox/branch/master/graph/badge.svg)](https://codecov.io/gh/kwonoj/rx-sandbox)
[![npm](https://img.shields.io/npm/v/rx-sandbox.svg)](https://www.npmjs.com/package/rx-sandbox)# RxSandbox
`RxSandbox` is test suite for RxJS, based on marble diagram DSL for easier assertion around Observables.
For RxJS 5 support, check pre-1.x versions. 1.x supports latest RxJS 6.x. 2.* supports [email protected] and above.## Maintenance notes
This project is still maintained. I consider this project as feature complete so there is not much activity going on. I'll be updating time to time as new upstream RxJS releases, but do not have much plan to add new features. If you're experiencing issues with latest RxJS, please open issue with reproducible test case. I'll try to fix as soon as possible.
## What's difference with `TestScheduler` in RxJS?
RxJs core itself includes its own [`TestScheduler`](https://github.com/ReactiveX/rxjs/blob/c63de0d380a923987aab587720473fad1d205d71/docs_app/content/guide/testing/marble-testing.md#testing-rxjs-code-with-marble-diagrams) implementation. With latest updates, it have different semantics around how to translate [`virtual frame` and `time progression`](https://github.com/ReactiveX/rxjs/blob/c63de0d380a923987aab587720473fad1d205d71/docs_app/content/guide/testing/marble-testing.md#time-progression-syntax) also slightly different api surfaces based on callbacks. RxSandbox aims to provide test interface with below design goals, bit different to core's test scheduler.
- Support extended marble diagram DSL
- Near-zero configuration, works out of box
- No dependencies to specific test framework
- Flexible `TestMessage` support: test can be created from marble syntax, but also can be created from plain objects.# Install
This has a peer dependencies of `rxjs`, which will have to be installed as well.
```sh
npm install rx-sandbox
```# Usage
## Observable marble diagram token description
In `RxSandbox`, `Observable` is represented via `marble` diagram. Marble syntax is a string represents events happening over `virtual` time so called as `frame`.
- `-` : Single `frame` of time passage, by default `1`.
- `|` : Successful completion of an observable signaling `complete()`.
- `#` : An error terminating the observable signaling `error()`.
- ` `(whitespace) : Noop, whitespace does nothing but allows align marbles for readability.
- `a` : Any other character than predefined token represents a value being emitted by `next()`
- `()` : When multiple events need to single in the same frame synchronously, parenthesis are used to group those events. You can group nexted values, a completion or an error in this manner. The position of the initial `(`determines the time at which its values are emitted.
- `^` : (hot observables only) Shows the point at which the tested observables will be subscribed to the hot observable. This is the "zero frame" for that observable, every frame before the `^` will be negative.
- `!` : (for subscription testing) Shows the point at which the tested observables will be unsubscribed.
- `...n...` : (`n` is number) Expanding timeframe. For cases of testing long time period in observable, can shorten marble diagram instead of repeating `-`.The first character of marble string always represents `0` frame.
### Few examples
```js
const never = `------`; // Observable.never() regardless of number of `-`
const empty = `|`; // Observable.empty();
const error = `#`; // Observable.throw(`#`);
const obs1 = `----a----`;
//` 01234 `, emits `a` on frame 4
const obs2 = `----a---|`;
//` 012345678`, emits `a` on frame 4, completes on 8
const obs2 = `-a-^-b--|`;
//` 012345`, emits `b` on frame 2, completes on 5 - hot observable only
const obs3 = `--(abc)-|`;
//` 012222234, emits `a`,`b`,`c` on frame 2, completes on 4
const obs4 = `----(a|)`;
//` 01234444, emits `a` and complets on frame 4
const obs5 = ` - --a- -|`;
//` 0 1234 56, emits `a` on frame 3, completes on frame 6
const obs6 = `--...4...--|`
//` 01......5678, completes on 8
```## Subscription marble diagram token description
The subscription marble syntax is slightly different to conventional marble syntax. It represents the **subscription** and an **unsubscription** points happening over time. There should be no other type of event represented in such diagram.
- `-` : Single `frame` of time passage, by default `1`.
- `^` : Shows the point in time at which a subscription happen.
- `!` : Shows the point in time at which a subscription is unsubscribed.
- (whitespace) : Noop, whitespace does nothing but allows align marbles for readability.
- `...n...` : (`n` is number) Expanding timeframe. For cases of testing long time period in observable, can shorten marble diagram instead of repeating `-`.There should be **at most one** `^` point in a subscription marble diagram, and **at most one** `!` point. Other than that, the `-` character is the only one allowed in a subscription marble diagram.
### Few examples
```js
const sub1 = `-----`; // no subscription
const sub2 = `--^--`;
//` 012`, subscription happend on frame 2, not unsubscribed
const sub3 = `--^--!-`;
//` 012345, subscription happend on frame 2, unsubscribed on frame 5
```## Anatomy of test interface
You can import `rxSandbox`, and create instance using `create()`.
```js
import { expect } from 'chai';
import { rxSandbox } from 'rx-sandbox';it('testcase', () => {
const { hot, cold, flush, getMessages, e, s } = rxSandbox.create();
const e1 = hot(' --^--a--b--|');
const e2 = cold(' ---x--y--|', {x: 1, y: 2});const expected = e(' ---q--r--|');
const sub = s(' ^ !');const messages = getMessages(e1.merge(e2));
flush();
//assertion
expect(messages).to.deep.equal(expected);
expect(e1.subscriptions).to.deep.equal(sub);
});
```### Creating sandbox
```typescript
rxSandbox.create(autoFlush?: boolean, frameTimeFactor?: number, maxFrameValue?: number): RxSandboxInstancerxSandbox.create({
autoFlush?: boolean,
frameTimeFactor?: number,
maxFrameValue?: boolean,
flushWithAsyncTick?: boolean,
}): RxSandboxInstance | RxAsyncSandboxInstance
````frameTimeFactor` allows to override default frame passage `1` to given value.
`maxFrameValue` allows to override maximum frame number testscheduler will accept. (`1000` by default). Maxframevalue is relavant to frameTimeFactor. (i.e if `frameTimeFactor = 2` and `maxFrameValue = 4`, `--` will represent max frame)Refer below for `autoFlush` option.
### Using RxSandboxInstance
`RxSandboxInstance` exposes below interfaces.
#### Creating hot, cold observable
```typescript
hot(marble: string, value?: { [key: string]: T } | null, error?: any): HotObservable;
hot(messages: Array>): HotObservable;cold(marble: string, value?: { [key: string]: T } | null, error?: any): ColdObservable;
cold(messages: Array>): ColdObservable;
```Both interfaces accepts marble diagram string, and optionally accepts custom values for marble values or errors. Otherwise, you can create `Array>` directly instead of marble diagram.
#### Creating expected value, subscriptions
To compare observable's result, we can use marble diagram as well wrapped by utility function to generate values to be asserted.
```typescript
e(marble: string, value?: { [key: string]: T } | null, error?: any): Array>;//const expected = e(`----a---b--|`);
```It accepts same parameter to hot / cold observable creation but instead of returning observable, returns array of metadata for marble diagram.
Subscription metadata also need to be generated via wrapped function.
```typescript
s(marble: string): SubscriptionLog;//const subs = s('--^---!');
```#### Getting values from observable
Once we have hot, cold observables we can get metadata from those observables as well to assert with expected metadata values.
```typescript
getMessages(observable: Observable, unsubscriptionMarbls: string = null): Array>>;const e1 = hot('--a--b--|');
const messages = getMessages(e1.mapTo('x'));//at this moment, messages are empty!
assert(messages.length === 0);
```It is important to note at the moment of getting metadata array, it is not filled with actual value but just empty array. Scheduler should be flushed to fill in values.
```typescript
const e1 = hot(' --a--b--|');
const expected = e('--x--x--|')
const subs = s(` ^ !`);
const messages = getMessages(e1.mapTo('x'));//at this moment, messages are empty!
expect(messages).to.be.empty;flush();
//now values are available
expect(messages).to.deep.equal(expected);
//subscriptions are also available too
expect(e1.subscriptions).to.deep.equal(subs);
```Or if you need to control timeframe instead of flush out whole at once, you can use `advanceTo` as well.
```typescript
const e1 = hot(' --a--b--|');
const subs = s(` ^ !`);
const messages = getMessages(e1.mapTo('x'));//at this moment, messages are empty!
expect(messages).to.be.empty;advanceTo(3);
const expected = e('--x------'); // we're flushing to frame 3 only, so rest of marbles are not constructed//now values are available
expect(messages).to.deep.equal(expected);
//subscriptions are also available too
expect(e1.subscriptions).to.deep.equal(subs);
```#### Flushing scheduler automatically
By default sandbox instance requires to `flush()` explicitly to execute observables. For cases each test case doesn't require to schedule multiple observables but only need to test single, we can create sandbox instance to flush automatically. Since it flushes scheduler as soon as `getMessages` being called, subsequent `getMessages` call will raise errors.
```typescript
const { hot, e } = rxSandbox.create(true);const e1 = hot(' --a--b--|');
const expected = e('--x--x--|')
const messages = getMessages(e1.mapTo('x'));//without flushing, observable immeditealy executes and values are available.
expect(messages).to.deep.equal(expected);//subsequent attempt will throw
expect(() => getMessages(e1.mapTo('y'))).to.throw();
```#### Scheduling flush into native async tick (Experimental, 2.0 only)
If you create sandbox instance with `flushWithAsyncTick` option, sandbox will return instance of `RxAsyncSandboxInstance` which all of flush interfaces need to be asynchronously awaited:
```
interface RxAsyncSandboxInstance {
...,
advanceTo(toFrame: number) => Promise;
flush: () => Promise;
getMessages: (observable: Observable, unsubscriptionMarbles?: string | null) => Promise;
}
```It is not uncommon practices chaining native async function or promise inside of observables, especially for inner observables. Let's say if there's a redux-observable epic like below
```
const epic = (actionObservable) => actionObservable.ofType(...).pipe((mergeMap) => {
return new Promise.resolve(...);
})
```Testing this epic via rxSandbox won't work. Once sandbox flush all internal actions synchronously, promises are still scheduled into next tick so there's no inner observable subscription value collected by flush. `RxAsyncSandboxInstance` in opposite no longer flush actions synchronously but schedule each individual action into promise tick to try to collect values from async functions.
**NOTE: this is beta feature and likely have some issues. Also until stablized internal implementation can change without semver breaking.**
#### Custom frame time factor
Each timeframe `-` is predefined to `1`, can be overridden.
```typescript
const { e } = rxSandbox.create(false, 10);const expected = e('--x--x--|');
// now each frame takes 10
expect(expected[0].frame).to.equal(20);
```#### Custom assertion for marble diagram
Messages generated by `rxSandbox` is plain object array, so any kind of assertion can be used. In addition to those, `rxSandbox` provides own custom assertion method `marbleAssert` for easier marble diagram diff.
```typescript
marbleAssert(source: Array>): { to: { equal(expected: Array>): void } }
```It accepts array of test messages generated by `getMessages` and `e`, or subscription log by `Hot/ColdObservable.subscriptions` or `s` (in case of utility method `s` it returns single subscription, so need to be constructed as array).
```typescript
import { rxSandbox } from 'rx-sandbox';
const { marbleAssert } = rxSandbox;const {hot, e, s, getMessages, flush} = rxSandbox.create();
const source = hot('---a--b--|');
const expected = e('---x--x---|');
const sub = s('^-----!');const messages = getMessages(source.mapTo('x'));
flush();marbleAssert(messages).to.equal(expected);
marbleAssert(messages).toEqual(expected); // if you prefer jasmin / jest style matcher syntax
marbleAssert(source.subscriptions).to.equal([sub]);
```When assertion fails, it'll display visual / object diff with raw object values for easier debugging.
**Assert Observable marble diagram**
**Assert subscription log marble diagram**
# Building / Testing
Few npm scripts are supported for build / test code.
- `build`: Transpiles code to ES5 commonjs to `dist`.
- `build:clean`: Clean up existing build
- `test`: Run unit test. Does not require `build` before execute test.
- `lint`: Run lint over all codebases
- `lint:staged`: Run lint only for staged changes. This'll be executed automatically with precommit hook.
- `commit`: Commit wizard to write commit message