Take a small Observable example from xstream - it outputs two numbers and completes when a second stream finishes after 5 seconds. It uses Observables and shows filtering and mapping values through the stream:
1 | import xs from 'xstream' |
Take a look at .filter
and .map
calls - doesn't it look like we are operating on a "plain"
Array? It certainly looks this way to me! And if Hey Underscore, You're Doing It Wrong!
taught me anything, it is that switching from a method to a function with
callback first is the way to go.
Babel-node
Before we proceed, let me show how I work with modern JavaScript in the terminal. I prefer to run every program locally, and avoid using online sandboxes like jsfiddle or plnkr - it is just faster to work locally for me!
In the example above we have everything modern Node 6/7 supports except for import
statement.
Thus we need to transpile this code before we can run it. I use Babel-node +
preset-env for this to avoid messing with
plugins / presets / features. So grab the above code, place it into index.js
and create a Node
package locally
1 | npm init --yes |
Then I define start
command to transpile code using current modern environment on run time.
1 | { |
The code works and prints 0
then 4
then completed
1 | npm start |
There are two second pauses between the numbers, so I am going to show the output like this
1 | 0 --- 4 --- completed -|-> |
The |
character means the end of the stream, and the ->
is the stream itself.
Perfect, now we can experiment
Named callbacks
The first thing I want to do is name and separate callback functions for clarity. We have several
1 | const isEven = i => i % 2 === 0 |
If we are doing this, let us quickly write a few unit tests - because we literally needs 10 seconds to do this. In our case I will use Ava - one of my favorite test runners lately.
Move callback functions into their own file
1 | // utils.js |
Install Ava (super simple)
1 | npm i -D [email protected] |
Since our code uses export
keyword, Ava needs to transpile code, and there are lot of ways
to configure it.
The simplest is to define the preset in the package.json
file
1 | { |
Let us write a test for isEven
function
1 | // test.js |
Hmm, kind of verbose, isn't it? Let us use Ava's snapshot feature to grab multiple values at once
1 | // test.js |
Look at the saved snapshot file
1 | $ npm test |
Nice, but less than perfect. Let us add an external snapshot testing library snap-shot (full disclosure: it is my baby!) that has data-driven testing inspired by sazerac.
1 | npm i -D snap-shot |
We need a named function isEven
, so I changed the arrow expression into a function
1 | // utils.js |
Because we are using only our assertion, and not Ava's built-in one, we need to allow Ava to pass such test (see the Ava configuration list)
1 | { |
The Ava test is now super compact
1 | // test.js |
The snapshot file makes it clear what is going on
1 | $ cat __snapshots__/test.js.snap-shot |
Ok, lovely! Similarly, we can test the square
function, if needed.
Bonus snap-shot
family includes
schema snapshot testing
and even subset shot testing.
Ramda instead of Observable
Look at our iteration over the events in the Observable
1 | const stream = xs.periodic(1000) |
It looks so much like Array.filter(...).map(...) ...
doesn't it?
Ramda library is perfect for replacing and extending built-in
JavaScript list iterators - and guess what - it can work with Observable! At least it can
understand methods like .map
and .filter
. Let us replace the fluent Observable
interface with a composition of R.map
and R.filter
calls.
1 | // index.js |
We replaced .map
and .filter
method calls with Ramda functions, and left .endWith
because there is no equivalent function.
The above code still runs the same and produces 0 --- 4 --- completed -|->
, yet
there is one interesting property. Because Ramda functions are curried, and we placed
callback functions at the first position, we moved the only "data" variable in the last
position, just like f(g(xs.periodic(1000)))
. We can compose map
and filter
calls
into a new function (I prefer using pipe
to compose
for clarity)
1 | import {filter, map, pipe} from 'ramda' |
Still works the same way, but why would I want to do this? Because aside from timing, I can
test the composed function evenSquare
easily. For example, if I move it to utils.js
I can write a snapshot test
1 | import {isEven, square, evenSquares} from './utils' |
What does it output? We can make it very visible using an environment variable while running tests
1 | $ SHOW=1 npm t |
Perfect. We just have rewritten our reactive pipeline to use battle-hardened, easy to unit test Ramda pipeline.