@7urtle/lambda
Effortless testing in JavaScript with functional programming

‹ Learn

Effortless testing in JavaScript with functional programming

Testing code written with functional programming and @7urtle/lambda requires the same testing tools that you are used to. Nothing new to learn. But functional programming makes the practice much easier. In the code examples bellow, we will be using Jest library but you can use any other that you like.

Testing pure functions

Functional programming leads you to the creation of simple pure functions that are stringed together by function composition. Pure functions are simply dependent only on their input without any side effects. You can read more about them in Functional programming basics. Common imperative functions often look like this:

1
let count = 0;
2
3
const badFunction = function () {
4
++count;
5
};

badFunction doesn't take any input and it just does things to change external variables. In functional programming that means that it is causing a side effect. That makes the function difficult to test. You always have to mock external dependencies and the function will have different effect depending on the current state of such dependencies. It should be obvious why large code base built this way would be hard to debug. Functional alternative would look like this:

1
const goodFunction = count => ++count;

goodFunction is a simple pure function that increases the value of its input. It will always return the same value for the same input which makes the test also very simple:

1
test('goodFunction increases its input without side effects.', () => {
2
let count = 1;
3
expect(goodFunction(count)).toBe(2);
4
expect(count).toBe(1);
5
});

Some side effects are of course unavoidable. In the end you want your code to change something whether it is a DOM element or a database record. For managing these we use monads.

Testing with monads

Monads represent simple wrappers around "dangerous" values and functions and provide safe way of mapping. To understand it more, consider this imperative function:

1
const getHeight = function (element) {
2
return document.querySelector(element).clientHeight;
3
};
4
5
getHeight('#myelement');

Coding with DOM in JavaScript is an example of working with side effects. querySelector may not find any element and return undefined which will result in the above code throwing an error. To handle this we need to add conditions and/or try catch statements.

In functional programming, just use Maybe monad as an argument for your function:

1
import {Maybe, map} from '@7urtle/lambda';
2
3
const selectDOMMaybe = selector => Maybe.of(document.querySelector(selector));
4
const getHeightMaybe = DOMMaybe => map(a => a.clientHeight)(DOMMaybe);
5
6
getHeightMaybe(selectDOMMaybe('#myelement')));

selectDOMMaybe and getHeightMaybe now work with our Maybe monad. You need to test selectDOMMaybe with DOM mock but any following function in the composition like getHeightMaybe can be tested independently now and very easily. These two functions also nicely demonstrate the benefit of separation of concerns that is discussed in detail in a moment.

getHeightMaybe can be tested without mocking DOM:

1
test('getHeightMaybe returns height if DOM element exists.', () => {
2
const mockJust = Maybe.of({clientHeight: 30});
3
const resultJust = getHeightMaybe(mockJust);
4
expect(resultJust.isJust()).toBe(true);
5
expect(resultJust.value).toBe(30);
6
});
7
8
test('getHeightMaybe is Nothing if DOM element is undefined.', () => {
9
const mockNothing = Maybe.of(undefined);
10
const resultNothing = getHeightMaybe(mockNothing);
11
expect(resultNothing.isNothing()).toBe(true);
12
});

Separation of concerns is another important programming principle that we can apply with ease.

Separation of concerns

Separation of concerns principle simply tells us to separate parts of code that deal with different things. In functional programming when you start learning the basics of pure functions you still need to learn how too deal with side effects.

Side effects are inevitable when you are integrating your code with the external world. That usually happens twice. At the beginning you need to get the input for your program and at the end yoo need to publish the result. By this logic we can separate our code into two concerns. Integration concern deals with reading and publishing changes and execution concern then deals with transformation of the input into the output.

In practice your code then should be divided into modules based on separation of concerns as well. Integrations should be tested by integration tests and execution can be covered by regular unit tests. In a code it could be expressed like this:

1
// ./FileReader.js //////
2
import {fs} from 'fs';
3
import {AsyncEffect} from '@7urtle/lambda';
4
5
// readFile :: string -> AsyncEffect
6
export const readFile => input =>
7
AsyncEffect
8
.of(reject => resolve => _ =>
9
fs.readFile(input, (err, data) =>
10
err ? reject(err) : resolve(data)
11
)
12
);
13
14
// ./DataTransform.js //////
15
import {UpperCaseOf, trim, compose, map} from '@7urtle/lambda';
16
17
// transformer :: string -> string
18
export const transformer = compose(upperCaseOf, trim);
19
20
// transformData :: AsyncEffect -> AsyncEffect
21
export const transformData = map(transformer);
22
23
// ./FileWriter.js //////
24
import {fs} from 'fs';
25
import {AsyncEffect, flatMap} from '@7urtle/lambda';
26
27
// writeFile :: string -> AsyncEffect -> AsyncEffect
28
export const writeFile = output =>
29
flatMap(data =>
30
AsyncEffect
31
.of(reject => resolve => _ =>
32
fs.writeFile(output, data, error =>
33
error ? reject(error) : resolve('success')
34
)
35
)
36
);
37
38
// ./index.js //////
39
import {log, compose} from '@7urtle/lambda';
40
import {readFile} from './FileReader';
41
import {transformData} from './DataTransform';
42
import {writeFile} from './FileWriter';
43
44
// execute :: string -> string -> AsyncEffect
45
const execute = input => output => compose(writeFile(output), transformData, readFile)(input);
46
47
execute('input.txt')('output.txt').trigger(log)(log)();

We end up with relatively short code that is divided into four files (modules). Everything is brought together in a final functional composition const execute = input => output => compose(writeFile(output), transformData, readFile)(input);.

Reading and writing into files is separated into individual modules that handle these side effects and the rest of the code (here simplified into transformData) is then pure and easy to test without any mocking like this:

1
import {AsyncEffect} from '@7urtle/lambda';
2
import {transformer, transformData} from './DataTransform';
3
4
test('transformer input goes through trim and upperCaseOf.', () => {
5
expect(transformer('input')).toBe('INPUT');
6
expect(transformer(' input ')).toBe('INPUT');
7
});
8
9
test('transformData maps transformer to AsyncEffect.', done => {
10
const input = AsyncEffect.of(reject => resolve => value => resolve(' input '));
11
12
transformData(input)
13
.trigger
14
(a => a)
15
(result => {
16
expect(result).toBe('INPUT');
17
done();
18
})
19
();
20
});

Notice that in the test we are happy to verify just the happy path of the AsyncEffect in the final function. We don't need to worry about error states because that is abstracted by the monads.

It is also important to mention that performance of execution can then be effortlessly optimized by caching memoization.

The documentation page on this website is generated out of JSDoc directly from the source code of @7urtle/lambda and the program uses the separation of concerns principle as it is described here when working with input files and the final output.

Ask For Help

If you have any questions or need any help, please leave a message on @7urtle/lambda GitHub Issues to get quick assistance. If you find any errors in @7urtle/lambda or on the website please reach out.

Get Started

Install @7urtle/lambda with NPM or add it directly to your website.