In this article we will focus on three points you should be aware of to use RxJS marble tests effectively.

TestScheduler is a powerful tool to test RxJS pipelines and operators. As you know, it makes tests declarative, simple, fast, and correct. In this article we will focus on three points you should be aware of to use RxJS marble tests effectively:

  • Can we use mutable data structure?
  • How to test pipeline based on Promise?
  • How to test meta and inner observables?

This is neither an introduction to RxJS nor to marble testing. Before reading the following sections you should be familiar with the basics of RxJS TestScheduler API. A good guide to marble testing is How to test Observables.

The source code is on Stackblitz.

Let’s start from the first point.

Mutability#

The best practice is to use immutable data structures and pure functions in RxJS pipelines. This keeps the code coherent with the Functional Programming mindset of the library and allows us to avoid subtle aliasing bugs. But this is not enforced by RxJS. So, we can use mutable objects.

Let’s take a look at the example:

const toggle = 
  () => pipe(
    scan((acc, value) => {
        const found = acc.indexOf(value) > -1;
        if (found) {
          acc.splice(acc.indexOf(value), 1);    
        } else {
          acc.push(value);
        }

        return acc;
    }, []) 
  );
<>

mutable toggle operator

The toggle operator checks if a received number already exists in the array accumulator. If it’s already there, it will be removed, otherwise it will be added to the array. So, if we test the operator with the following code:

of(1, 2, 3, 2, 3, 1).pipe(
  toggle()
)
.subscribe(x => console.log(x));
<>

The result is the following:

[1]
[1, 2]
[1, 2, 3]
[1, 3]
[1]
[]
<>

So, the toggle is doing the right job and the printed result is correct.

Now, if we write the following marble test, it will fail:

testScheduler.run(({ expectObservable }) => {
    const source =  of(1, 2, 3, 2, 3, 1);

    const result = source.pipe(toggle());

    expectObservable(result).toBe(
    	'(abcdef|)',
        {a:[1], b:[1,2], c:[1,2,3], d:[1,3], e:[1], f:[]}
    );
});
<>

The test fails because the TestScheduler does not take a snapshot of the object’s state. Instead, it just keeps a reference to the received object (here is the source code). So, if we mutate the object stored by RxJS, the test will be incorrect even if it passes.

To fix the code and its test, we convert the scan callback to a pure function:

const toggle = 
  () => pipe(
    scan((acc, value) => {
        const found = acc.indexOf(value) > -1;
        if (found) {
          return acc.filter(v => v !== value);    
        } else {
          return [...acc, value];
        }
    }, []) 
  );
<>

Now that the marble test is passing, let’s jump to a real limitation.

Promise#

RxJS has a good integration with Promises. An observable can easily be converted to a Promise and vice versa. However, taking advantage of this relationship prevents us from using the power of marble tests.

Let’s explain this point using a fictitious example. The following RedisCache is a third-party API that we use to store data into a remote Redis memory cache. The RedisCache is Promise-based API client.

interface RedisCache {
  get(...keys: string[]): Promise<Record<string, string>>;
}
<>

When possible, we want to use only Observables in our source code. Therefore, we should not use this API directly in our codebase.

class RemoteCache {

  constructor(private redis: RedisCache) {    
  }

  get(key: string): Observable<{key: string; value: string;}> {
    return from(this.redis.get(key))
      .pipe(
        map(value => ({key: key, value: value[key]}))
      );
  }
}

#reactive-programming #rxjs #testing

Effective RxJS Marble Testing
1.45 GEEK