What Is The State Of The Art For Testing/mocking Functions Within A Module In 2018?
Solution 1:
As you've already discovered attempting to directly test an ES6 module is extremely painful. In your situation it sounds like you are transpiling the ES6 module rather than testing it directly, which would likely generate code that looks something like this:
asyncfunctionmakeApiCall(uri) {
...
}
module.exports.makeApiCall = makeApiCall;
Since the other methods are calling makeApiCall
directly, rather than the export, even if you tried to mock the export nothing would happen. As it stands ES6 module exports are immutable, so even if you did not transpile the module you would likely still have issues.
Attaching everything to a "lib" object is probably the easiest way to get going, but it feels like a hack, not a solution. Alternatively using a library that can rewire the module is a potential solution, but its extremely hokey and in my opinion it smells. Usually when you're running into this type of code smell you have a design problem.
Splitting up the modules into tiny pieces feels like a poor mans dependency injection, and as you've stated you'll likely run into issues quickly. Real dependency injection is probably the most robust solution, but it's something you need to build from the ground up, it's not something that you can just plug into an existing project and expect to have things working immediately.
My suggestion? Create classes and use them for testing instead, then just make the module a thin wrapper over an instance of the class. Since you're using a class you'll always be referencing your method calls using a centralized object (the this
object) which will allow you to mock out the things you need. Using a class will also give you an opportunity to inject data when you construct the class, giving you extremely fine grained control in your tests.
Let's refactor your api
module to use a class:
import axios from'axios';
exportclassApiClient {
constructor({baseUrl, client}) {
this.baseUrl = baseUrl;
this.client = client;
}
asyncmakeApiCall(uri) {
try {
const response = awaitthis.client(`${this.baseUrl}${uri}`);
return response.data;
} catch (err) {
throw err.message;
}
}
asyncfetchUsers() {
returnthis.makeApiCall('/users');
}
asyncfetchUser(id) {
returnthis.makeApiCall(`/users/${id}`);
}
asyncfetchUserStrings(...ids) {
const users = awaitPromise.all(ids.map(id =>this.fetchUser(id)));
return users.map(user =>this.parseUser(user));
}
parseUser(user) {
return`${user.name}:${user.username}`;
}
}
exportdefaultnewApiClient({
url: "https://jsonplaceholder.typicode.com/",
client: axios
});
Now lets create some tests for the ApiClient
class:
import {ApiClient} from'./api';
describe('api tests', () => {
let api;
beforeEach(() => {
api = newApiClient({
baseUrl: 'http://test.com',
client: jest.fn()
});
});
it('makeApiCall should use client', async () => {
const response = {data: []};
api.client.mockResolvedValue(response);
const value = await api.makeApiCall('/foo');
expect(api.client).toHaveBeenCalledWith('http://test.com/foo');
expect(value).toBe(response.data);
});
it('fetchUsers should call makeApiCall', async () => {
const value = [];
jest.spyOn(api, 'makeApiCall').mockResolvedValue(value);
const users = await api.fetchUsers();
expect(api.makeApiCall).toHaveBeenCalledWith('/users');
expect(users).toBe(value);
});
});
I should note that I have not tested if the provided code works, but hopefully the concept is clear enough.
Post a Comment for "What Is The State Of The Art For Testing/mocking Functions Within A Module In 2018?"