Mocking JSforce with Jest
by Patrick Connelly posted on March 30, 2021
Testing is one of the most important things you can do your code. However, when using a library that requires access to a remote system unit testing can become problematic. You don’t want to have to have a real connection and pull down real data since this is slow and requires anyone contributing to your project to set up the remote system as well. This is where mocking comes in with Jest.
Mocking allows you to detach a module from making a remote call and allows you to define the data that it returns. This means that you can modify the data how you need to provide a small data set or a large data set or even error out on demand.
In this post, we’re going to look at how to set up mocking for JSforce to test logging in and mocking out making queries.
Javascript module
Let’s create a simple Javascript module that logs into Salesforce and takes that connection to make a query and returns results.
const jsforce = require('jsforce');
/**
* Logs in using environment variables
* @returns {Promise} A promise for the connection
*/
function login() {
return new Promise(function (resolve, reject) {
const conn = new jsforce.Connection();
conn.login(
process.env.SFDC_USERNAME,
process.env.SFDC_PASSWORD,
function (error) {
if (error) {
reject(error);
} else {
resolve(conn);
}
}
);
});
}
/**
* Queries Salesforce
* @param {String} query_string The query
* @returns {Promise} A promise for the query results
*/
function query(query_string) {
return new Promise(function (resolve, reject) {
login()
.then(function (conn) {
conn.query(query_string, function (error, results) {
if (error) {
reject(error);
} else {
resolve(results);
}
});
})
.catch(reject);
});
}
module.exports = {
login: login,
query: query
};
Mocking JSforce
Now we need to mock this out so that it never tries to create a connection to Salesforce when we call the login
function
const jsforce = jest.createMockFromModule('jsforce');
const __loginMock = jest.fn(function (user, pass, cb) {
cb();
});
const __queryMock = jest.fn(function (query_string, cb) {
cb();
});
jsforce.__loginMock = __loginMock;
jsforce.__queryMock = __queryMock;
jsforce.Connection = jest.fn().mockImplementation(() => {
return {
login: jsforce.__loginMock,
query: jsforce.__queryMock
};
});
module.exports = jsforce;
Let’s break down this mock to explain what we’re doing.
const __loginMock = jest.fn(function (user, pass, cb) {
cb();
});
const __queryMock = jest.fn(function (query_string, cb) {
cb();
});
Here we define mocked functions that no matter what is passed in, we call the cb()
function without any results so this will count as a “successful” login or a query with no results
jsforce.__loginMock = __loginMock;
jsforce.__queryMock = __queryMock;
jsforce.Connection = jest.fn().mockImplementation(() => {
return {
login: jsforce.__loginMock,
query: jsforce.__queryMock
};
});
Here we set our __loginMock
and __queryMock
functions to be able to access it in our test. We then also mock out the Connection
object so that we return our new login and query functions as part of our connection.
This mock is the bare minimum we need but it doesn’t actually do anything. We will re-mock these function in our tests to change their behavior. But if we don’t mock it now then we could accidentally call a function
Jest tests
const index = require('..');
const jsforce = require('jsforce');
const SFDC_USERNAME = 'bob@example.com';
const SFDC_PASSWORD = 'test123';
jest.mock('jsforce');
describe('login', function () {
beforeEach(function () {
process.env = {
SFDC_USERNAME: SFDC_USERNAME,
SFDC_PASSWORD: SFDC_PASSWORD
};
jsforce.__loginMock = jest.fn(function (user, pass, cb) {
cb();
});
});
afterEach(function () {
jest.clearAllMocks();
});
test('valid login', function () {
expect.assertions(2);
return index.login().then(function () {
return new Promise(function (resolve) {
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
});
test('login failed', function () {
expect.assertions(3);
jsforce.__loginMock = jest.fn(function (user, pass, cb) {
cb(new Error('Invalid password'));
});
return index.login().catch(function (error) {
return new Promise(function (resolve) {
expect(error.message).toBe('Invalid password');
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
});
});
describe('query', function () {
beforeEach(function () {
process.env = {
SFDC_USERNAME: SFDC_USERNAME,
SFDC_PASSWORD: SFDC_PASSWORD
};
jsforce.__loginMock = jest.fn(function (user, pass, cb) {
cb();
});
jsforce.__queryMock = jest.fn(function (query_string, cb) {
cb();
});
});
afterEach(function () {
jest.clearAllMocks();
});
test('login failed', function () {
expect.assertions(3);
jsforce.__loginMock = jest.fn(function (user, pass, cb) {
cb(new Error('Invalid password'));
});
return index.query().catch(function (error) {
return new Promise(function (resolve) {
expect(error.message).toBe('Invalid password');
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
});
test('results', function () {
expect.assertions(5);
const data = [
{
Id: '1234567890abcdefg1',
Name: 'Example1'
},
{
Id: '1234567890abcdefg2',
Name: 'Example2'
}
];
jsforce.__queryMock = jest.fn(function (query_string, cb) {
cb(undefined, data);
});
const query_string = 'select Id, Name from Account';
return index.query(query_string).then(function (results) {
return new Promise(function (resolve) {
expect(results).toEqual(data);
expect(jsforce.__queryMock).toHaveBeenCalled();
expect(jsforce.__queryMock).toHaveBeenCalledWith(query_string, expect.anything());
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
});
test('error', function () {
expect.assertions(5);
jsforce.__queryMock = jest.fn(function (query_string, cb) {
cb(new Error('Invalid query'));
});
const query_string = 'select Id, Name from Account';
return index.query(query_string).catch(function (error) {
return new Promise(function (resolve) {
expect(error.message).toBe('Invalid query');
expect(jsforce.__queryMock).toHaveBeenCalled();
expect(jsforce.__queryMock).toHaveBeenCalledWith(query_string, expect.anything());
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
});
});
This test class contains two groupings of tests (login and query) and then five total tests for these. Let’s start by looking at the login tests to see how we can use mocks to test our login call
jsforce.__loginMock = jest.fn(function (user, pass, cb) {
cb();
});
In our beforeEach
we reset our login function to the default so that it will always be “successful”
test('valid login', function () {
expect.assertions(2);
return index.login().then(function () {
return new Promise(function (resolve) {
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
});
Then in our valid test we call our login function and check to make sure it was called with what we expected.
Now for our failed test, we need to change how the mock works
jsforce.__loginMock = jest.fn(function (user, pass, cb) {
cb(new Error('Invalid password'));
});
Here we make our callback with an error so that our main code will reject the promise.
return index.login().catch(function (error) {
return new Promise(function (resolve) {
expect(error.message).toBe('Invalid password');
expect(jsforce.__loginMock).toHaveBeenCalled();
expect(jsforce.__loginMock).toHaveBeenCalledWith(SFDC_USERNAME, SFDC_PASSWORD, expect.anything());
resolve();
});
});
Here we call catch
and check that the error.message
is what we expect and that the mock function was called with the right parameters.
We do similar things with our query functions either returning our data
or by making a callback with an error that the promise then rejects.
Conclusion
Once you get the framework for mocking calls down, it’s not super difficult to reproduce. It can be a bit time consuming and a bit fiddly in spots but it’s worth being able fully test your code. Just remember to create your module mock in __mocks__
and then overwrite your mocked function in your test to return the data you need.