I've been playing around with functional programming and testing with jest recently.
An issue I've run into, is that it's hard to mock ES6 module exports directly. See this Stack Overflow question for more details.
The solution I've come up with is to inject the dependency functions into the function that is calling it, via .bind
.
Here's an example:
import axios from "axios";
const BASE_URL = "https://jsonplaceholder.typicode.com/";
const URI_USERS = 'users/';
/**
* These are the implementation of our functions, which take functions as arguments.
*/
export const _fetchUsers = async function (_makeApiCall, _URI_USERS) {
return _makeApiCall(_URI_USERS);
}
export const _fetchUser = async function (_makeApiCall, _URI_USERS, id) {
return _makeApiCall(_URI_USERS + id);
}
export const _fetchUserStrings = async function (_fetchUser, _parseUser, ...ids) {
const users = await Promise.all(ids.map(id => _fetchUser(id)));
return users.map(user => _parseUser(user));
}
export const _makeApiCall = async function (uri) {
try {
const response = await axios(BASE_URL + uri);
return response.data;
} catch (err) {
throw new Error(err.message);
}
}
export function _parseUser(user) {
return `${user.name}:${user.username}`;
}
/**
* Real exports
*/
export const makeApiCall = _makeApiCall;
export const parseUser = _parseUser;
export const fetchUsers = _fetchUsers.bind(null, _makeApiCall, URI_USERS);
export const fetchUser = _fetchUser.bind(null, _makeApiCall, URI_USERS);
export const fetchUserStrings = _fetchUserStrings.bind(null, _fetchUser, _parseUser);
And a test:
/**
* Our most complicated test
*
*/
describe("_fetchUserStrings", () => {
describe("happy flow", () => {
const fetchUserMock = jest.fn((i) => Promise.resolve({
username: "foo",
name: "bar"
}));
const parseUserMock = jest.fn(user => "string");
const fetchUserStrings = _fetchUserStrings.bind(null, fetchUserMock, parseUserMock);
it("returns an array of three strings", async () => {
expect.assertions(3);
const result = await fetchUserStrings(1, 2, 3);
// I'm being a bit lazy here, you could be checking that
// The strings are actually there etc, but whatevs.
expect(fetchUserMock).toHaveBeenCalledTimes(3);
expect(parseUserMock).toHaveBeenCalledTimes(3);
expect(result).toHaveLength(3);
})
});
});
My question is - is this a super clean way of writing functional JavaScript, or is this overkill that is easily solved another way?
If you're interested, there are more examples here.