Unit Testing Node.js with Jest
Test Node.js code that talks to SQL Server and reads XML with Jest, using explicit mocks, working examples for a user lookup and an XML validator, and tests for the failure cases as well as the happy path.

Unit testing matters most where your code touches things you do not control: a database and data that arrives as XML. If either one behaves badly in production, you want a test to have caught it first.
This post is for a Node.js developer who knows basic SQL and XML and wants to test backend code with Jest. By the end you will be able to set Jest up, test a SQL query without a real database, validate that an XML document has the fields it needs, and run one test file at a time.
Every example here is complete. You can copy a file as written and run it.
Prerequisites
You need:
- A Node.js application, or an empty folder to start one in
- A basic understanding of SQL and XML
- Node.js and npm installed
You do not need a running SQL Server. The SQL tests mock the database, which is the point of them.
Project structure
The examples use this layout. Keep the test file beside the file it tests, so Jest finds it with no extra configuration.
my-app/
package.json
.env
db.js
userService.js
userService.test.js
xmlValidator.js
xmlValidator.test.js
Jest runs any file that ends in .test.js by default. There is no Jest config file in this post, because the defaults are enough for these examples.
Install Jest and the other packages
Install Jest as a development dependency, and mssql and fast-xml-parser as normal dependencies.
npm init -y
npm install --save-dev jest
npm install mssql fast-xml-parser
Add the test scripts to package.json. This is the complete file after npm init -y, with the scripts added:
{
"name": "my-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"dependencies": {
"fast-xml-parser": "^4.5.0",
"mssql": "^11.0.1"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
Your version numbers may differ. What matters is the scripts block: npm test now runs Jest, npm run test:watch reruns tests when a file changes, and npm run test:coverage reports which lines your tests never reached.
Test a SQL query without a database
The example is a user lookup: getUserById takes an ID, queries SQL Server, and returns one user. The test checks what the function does with the result, without connecting to anything.
Step 1: Create the database module
Put the connection code in its own file, db.js, so the service and the tests have one place to replace.
Read the credentials from environment variables. Never put a real username or password in code that can be committed to a repo.
// db.js
const sql = require('mssql');
const config = {
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
server: process.env.DB_SERVER,
database: process.env.DB_NAME,
options: {
encrypt: true,
enableArithAbort: true,
},
};
const connectToDB = async () => {
try {
const pool = await sql.connect(config);
return pool;
} catch (err) {
console.error('Database connection failed:', err);
throw err;
}
};
module.exports = { connectToDB };
Set the four variables in a local .env file for development, or in your deployment settings. Keep .env out of version control.
DB_USER=yourUsername
DB_PASSWORD=yourPassword
DB_SERVER=yourServer
DB_NAME=yourDatabase
One limit to know about: connectToDB opens a connection each time it is called and does not close it. That is fine for this example and for the tests, which never call it for real. In a real application, reuse one pool across requests instead of connecting per call, and close it when the app shuts down, with await sql.close().
Step 2: Write the function to fetch data
userService.js uses sql.Int, so it must import mssql itself. Importing it only in the test file is not enough: the test would pass while the real code failed with sql is not defined.
// userService.js
const sql = require('mssql');
const { connectToDB } = require('./db');
const getUserById = async (userId) => {
const pool = await connectToDB();
const result = await pool
.request()
.input('userId', sql.Int, userId)
.query('SELECT * FROM Users WHERE id = @userId');
if (result.recordset.length === 0) {
return null;
}
return result.recordset[0];
};
module.exports = { getUserById };
The empty check matters. Without it, a missing user returns undefined, and the caller cannot tell a missing user apart from a bug. Returning null gives the caller one clear not found value to test for.
Step 3: Mock the database module in Jest
Mock ./db, the module userService actually depends on, and mock mssql with a factory that supplies the one thing the service uses from it, sql.Int. Define both mocks yourself. Do not use jest.mock('mssql') on its own and rely on an auto-mock: an auto-mocked sql.Int may not behave like the real type, so an assertion on it can pass or fail for the wrong reason. A factory mock has a second benefit: Jest never loads the real driver, so a driver or dependency update cannot break the unit tests.
// userService.test.js
const sql = require('mssql');
const { connectToDB } = require('./db');
const { getUserById } = require('./userService');
jest.mock('./db', () => ({
connectToDB: jest.fn(),
}));
jest.mock('mssql', () => ({
Int: 'Int',
}));
describe('User Service', () => {
let mockRequest;
let mockPool;
beforeEach(() => {
jest.clearAllMocks();
mockRequest = {
input: jest.fn().mockReturnThis(),
query: jest.fn(),
};
mockPool = {
request: jest.fn().mockReturnValue(mockRequest),
};
connectToDB.mockResolvedValue(mockPool);
});
it('should return user data for a valid userId', async () => {
const mockData = { id: 1, name: 'John Doe' };
mockRequest.query.mockResolvedValue({ recordset: [mockData] });
const result = await getUserById(1);
expect(result).toEqual(mockData);
expect(mockPool.request).toHaveBeenCalled();
expect(mockRequest.input).toHaveBeenCalledWith('userId', sql.Int, 1);
expect(mockRequest.query).toHaveBeenCalledWith(
'SELECT * FROM Users WHERE id = @userId'
);
});
it('should return null when no user is found', async () => {
mockRequest.query.mockResolvedValue({ recordset: [] });
const result = await getUserById(999);
expect(result).toBeNull();
});
it('should throw when the database query fails', async () => {
mockRequest.query.mockRejectedValue(new Error('Query failed'));
await expect(getUserById(1)).rejects.toThrow('Query failed');
});
});
jest.clearAllMocks() in beforeEach stops one test leaking its mock behaviour into the next. That leak is a common reason a suite passes in one order and fails in another.
The three tests cover the three things this function can do: return a user, find no user, or fail.
Test XML validation
The second example checks that an XML document has the fields it needs. Each <user> needs an <id>, a <name> and an <email>.
Step 1: Write the XML validator
validateMandatoryFields now returns a result object, not just true or false, so a failure tells you which fields are missing and why.
// xmlValidator.js
const { XMLParser, XMLValidator } = require('fast-xml-parser');
const findMissingFields = (jsonObj, requiredFields) => {
return requiredFields.filter((field) => {
const parts = field.split('.');
let obj = jsonObj;
for (const part of parts) {
if (obj === undefined || obj === null || obj[part] === undefined) {
return true;
}
obj = obj[part];
}
return false;
});
};
const validateMandatoryFields = (xmlData, requiredFields) => {
const validation = XMLValidator.validate(xmlData);
if (validation !== true) {
return {
isValid: false,
missingFields: [...requiredFields],
error: validation.err.msg,
};
}
const parser = new XMLParser();
const jsonObj = parser.parse(xmlData);
const missingFields = findMissingFields(jsonObj, requiredFields);
return {
isValid: missingFields.length === 0,
missingFields,
};
};
module.exports = { validateMandatoryFields };
Three details in this function matter, and each one covers a real case:
- A field counts as missing only when it is
undefined. Checking truthiness instead would report<id>0</id>as missing, because0is falsy, and the same for an empty string orfalse. XMLValidator.validate()checks the XML is well formed before it is parsed. Malformed XML returnsisValid: falsewith the parser error inerror, instead of being parsed into an unexpected shape.- The result names the missing fields. A bare
falseleaves you guessing which one of ten fields failed.
Step 2: Write the tests for XML validation
// xmlValidator.test.js
const { validateMandatoryFields } = require('./xmlValidator');
const requiredFields = ['user.id', 'user.name', 'user.email'];
describe('XML Validator', () => {
it('should pass if all mandatory fields are present', () => {
const xmlData = `
<user>
<id>1</id>
<name>John Doe</name>
<email>johndoe@example.com</email>
</user>
`;
const result = validateMandatoryFields(xmlData, requiredFields);
expect(result.isValid).toBe(true);
expect(result.missingFields).toEqual([]);
});
it('should pass when a field value is 0', () => {
const xmlData = `
<user>
<id>0</id>
<name>John Doe</name>
<email>johndoe@example.com</email>
</user>
`;
const result = validateMandatoryFields(xmlData, requiredFields);
expect(result.isValid).toBe(true);
});
it('should fail and name the field if a mandatory field is missing', () => {
const xmlData = `
<user>
<id>1</id>
<name>John Doe</name>
</user>
`;
const result = validateMandatoryFields(xmlData, requiredFields);
expect(result.isValid).toBe(false);
expect(result.missingFields).toEqual(['user.email']);
});
it('should fail if the XML is not well formed', () => {
const xmlData = '<user><id>1</id>';
const result = validateMandatoryFields(xmlData, requiredFields);
expect(result.isValid).toBe(false);
expect(result.error).toBeDefined();
});
});
The 0 test is the one that protects the fix. Without it, someone can change the check back to truthiness later and no test will notice.
Run the tests
Run every test with:
npm test
To run one test file, use npx jest with the file name. npm jest is not a valid command: npm runs scripts and packages, it does not run Jest directly.
npx jest userService.test.js
You can also pass a pattern, and Jest runs every test file whose name matches it:
npx jest user
That runs userService.test.js, and would also run any other file with user in its name.
If Jest is set up as the test script, the same file can be run through npm by passing the argument after --:
npm test -- userService.test.js
Use the npx form while you are learning. It says exactly what is running.
Mock, or use a real database?
Mock in a unit test, and use a real database in an integration test. They answer different questions.
- A unit test with a mock answers: does my code handle the result correctly? It is fast, it needs no database, and it can force failures that are hard to arrange for real, like a rejected query.
- An integration test against a real test database answers: does my SQL actually work on SQL Server? It catches a wrong column name or invalid syntax, which a mock can never catch, because the mock returns whatever you told it to.
So keep both, in separate files or separate runs. Do not let a mocked suite be the only check on real SQL, and do not make unit tests wait on a database.
Checklist
- Import everything a file uses in that file, including
sqlinuserService.js. - Keep credentials in environment variables, never in source code.
- Mock the modules your code depends on, define the mocks yourself, and clear mocks in
beforeEach. - Test the failure cases: no record found, a rejected query, a missing field, malformed XML.
- Check for
undefinedwhen you test whether a field is present, not truthiness. - Run tests with
npm test, or one file withnpx jest.
What to try next
Take one function you already have in production and write the failure test for it first: the empty result, the error, or the malformed input, whichever one would hurt most if it reached a user. That single test usually teaches you more about your own code than ten happy path tests, and it tells you where the next guard, like the null return in getUserById, needs to go.
About the author
- I am an Engineer in love with software and writing code. I am super passionate about the reach of technology, and when it comes to conquering new frontiers, I want to be there in the forefront.



