Automated Testing in ServiceNow: Unit Tests, Jest, and CI/CD Pipelines
Most ServiceNow developers don't write tests. They deploy to a dev instance, click around manually, and pray nothing breaks in production. It works until it doesn't — and when it doesn't, you're debugging a live instance at 2 AM while users are filing incidents.
The good news: ServiceNow has a genuine testing ecosystem. The bad news: most developers never use it. This guide changes that. By the end, you'll have a testing practice that catches bugs before they leave your IDE, not after they take down an ITSM workflow.
The ServiceNow Testing Landscape
ServiceNow testing isn't one thing — it's several layers, each solving a different problem:
- QUnit — Client-side unit tests for browser code (Catalog Client Scripts, UI Policies, UI Actions)
- Jest — Server-side unit tests for Script Includes, Business Rules, and scoped apps
- Automated Test Framework (ATF) — End-to-end functional tests that simulate real user workflows
- Test Management — Test planning, test case management, and result tracking tied to requirements
- ServiceNow CLI (
sn) — Local test execution with CI/CD integration
Each layer serves a different purpose. You don't need all of them on day one — but you need more than zero.
Writing Server-Side Unit Tests with Jest
Jest is the go-to for testing server-side ServiceNow code. The ServiceNow CLI (sn dys) scaffolds Jest automatically when you create a scoped app.
Project Setup
# Install the ServiceNow CLI
npm install -g @servicenow/cli
# Create a scoped app (if you don't have one)
sn project create my-scope
# Navigate to the app directory
cd my-scope
# Run all tests
sn test run
When you scaffold a scoped app, the CLI creates a tests/ directory with sample test files. Each test file lives alongside the source file it tests:
src/
script_includes/
MyUtils.js
tests/
script_includes/
MyUtils.test.js
Writing Your First Test
Here's a Script Include that formats currency:
// src/script_includes/CurrencyFormatter.js
class CurrencyFormatter {
format(amount, currencyCode = 'USD') {
if (typeof amount !== 'number' || isNaN(amount)) {
return '';
}
const formatter = new sn_utils.GlideCurrencyFormatter();
formatter.setCurrencyCode(currencyCode);
return formatter.format(amount);
}
}
And its corresponding Jest test:
// tests/script_includes/CurrencyFormatter.test.js
const CurrencyFormatter = require('../../src/script_includes/CurrencyFormatter');
describe('CurrencyFormatter', () => {
let formatter;
beforeEach(() => {
formatter = new CurrencyFormatter();
});
test('formats USD correctly', () => {
const result = formatter.format(1234.56, 'USD');
expect(result).toBe('$1,234.56');
});
test('returns empty string for NaN', () => {
const result = formatter.format(NaN, 'USD');
expect(result).toBe('');
});
test('returns empty string for non-numeric input', () => {
const result = formatter.format('not a number', 'USD');
expect(result).toBe('');
});
test('defaults to USD when no currency code provided', () => {
const result = formatter.format(99.99);
expect(result).toContain('99.99');
});
});
Run it with sn test run and you'll get output like:
PASS tests/script_includes/CurrencyFormatter.test.js
CurrencyFormatter
✓ formats USD correctly
✓ returns empty string for NaN
✓ returns empty string for non-numeric input
✓ defaults to USD when no currency code provided
Mocking GlideSystem and Other Server APIs
The trickiest part of Jest testing in ServiceNow is mocking platform APIs. The ServiceNow CLI provides mocking utilities for common globals:
// tests/script_includes/MyBusinessRule.test.js
const { mockGlideSystem } = require('@servicenow/cli/test-utils');
describe('MyBusinessRule', () => {
let mockGS;
beforeEach(() => {
mockGS = mockGlideSystem();
});
test('logs warning when priority is high', () => {
mockGS.setProperty('x_my_scope.high_priority_threshold', '1');
const result = processRecord({ priority: 1 });
expect(mockGS.log).toHaveBeenCalledWith(
expect.stringContaining('High priority incident')
);
});
});
The @servicenow/cli/test-utils package provides mocks for GlideSystem, GlideRecord, GlideAjax, and more. Check the ServiceNow Developer Docs for the full mock API.
Writing Client-Side Unit Tests with QUnit
QUnit tests run in the browser context — they're for Client Scripts, UI Policies, and other client-side logic. In a scoped app project, QUnit tests live in tests/client/:
// tests/client/CatalogClientScript.test.js
describe('ItemSelectorClientScript', function() {
it('should populate options when category changes', function() {
// Simulate the onChange event
var event = {
newValue: 'hardware',
oldValue: ''
};
var result = onCategoryChange(event);
expect(result.optionsPopulated).toBe(true);
});
it('should clear options when category is empty', function() {
var event = {
newValue: '',
oldValue: 'software'
};
var result = onCategoryChange(event);
expect(result.optionsPopulated).toBe(false);
expect(result.optionsCleared).toBe(true);
});
});
Run QUnit tests from your instance by navigating to System Definition > Unit Tests, or trigger them via the CLI with sn test run --suite qunit.
End-to-End Testing with ATF
ATF (Automated Test Framework) is for integration-level tests — the kind that log in as a user, fill out a catalog form, submit it, and verify the incident was created correctly. QUnit and Jest test units in isolation; ATF tests workflows end-to-end.
Creating an ATF Test Suite
- Go to Automated Test Framework > Create Test
- Give it a name and link it to the application scope
- Add test steps — each step is a recorded action or assertion
Typical ATF test steps:
- Open a form (e.g., open the Service Catalog)
- Set field values (e.g., fill in variables)
- Click a button (e.g., Submit)
- Assert field values (e.g., verify incident state is "New")
- Assert record exists (e.g., verify incident was created in the database)
Running ATF Tests from the CLI
ATF tests can be triggered from the ServiceNow CLI:
sn test run --suite atf --target https://your-instance.service-now.com
This is where CI/CD integration becomes powerful — you can run ATF suites against a staging instance as part of your deployment pipeline.
Building a CI/CD Pipeline with GitHub Actions
Here's a GitHub Actions workflow that runs Jest unit tests on every push, then deploys if tests pass:
# .github/workflows/test-and-deploy.yml
name: Test and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install ServiceNow CLI
run: npm install -g @servicenow/cli
- name: Run Jest unit tests
run: sn test run --format junit --output test-results/junit.xml
env:
SN_HOST: ${{ secrets.SN_HOST }}
SN_USERNAME: ${{ secrets.SN_USERNAME }}
SN_PASSWORD: ${{ secrets.SN_PASSWORD }}
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: jest-results
path: test-results/junit.xml
deploy:
needs: unit-tests
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Install ServiceNow CLI
run: npm install -g @servicenow/cli
- name: Deploy to production
run: sn deploy --app x_my_scope --target ${{ secrets.SN_HOST }}
env:
SN_HOST: ${{ secrets.SN_HOST }}
SN_USERNAME: ${{ secrets.SN_USERNAME }}
SN_PASSWORD: ${{ secrets.SN_PASSWORD }}
The key pieces:
sn test runexecutes Jest tests locally, no instance required for pure unit tests--format junitoutputs JUnit XML, which GitHub Actions parses natively- Secrets store your instance credentials securely — never hardcode credentials
needs: unit-testsgates deployment on test success
Test Management: Linking Tests to Requirements
Test Management (available in the Test Management plugin) adds the organizational layer on top of raw tests. It lets you:
- Create test plans tied to stories or requirements
- Record manual test steps alongside automated ones
- Track test coverage — what percentage of your CI classes have tests?
- Generate QA reports for sprint reviews or audits
For teams practicing BDD or formal QA, Test Management is worth the setup time. For smaller teams, the combination of Jest + ATF + GitHub Actions covers 90% of what you actually need.
What Not to Test
Testing everything is a trap. Focus your effort on:
Worth testing:
- Script Includes with business logic
- Transform map scripts
- ACL scripts with complex conditions
- Workflow conditions and branch logic
- Service Catalog client scripts
Not worth testing:
- Simple UI Actions that call
gs.addInfoMessage() - Decorative UI policies
- Records that exist purely for reference data
- One-liner Business Rules that just set a field value
The goal is confidence, not coverage. A well-tested Script Include is worth more than ten tests on trivial UI Actions.
Getting Started Today
You don't need to build the whole testing pyramid on day one. Start here:
- Today: Install the ServiceNow CLI and run
sn test runon your existing scoped app. You probably already have sample tests — run them. - This week: Write 3 Jest tests for your most fragile Script Include. Pick the one that breaks most often after deployments.
- This month: Add a GitHub Actions workflow that runs tests on every PR. Set it to block merges when tests fail.
- This quarter: Add one ATF test suite for your most critical workflow (incident creation, change approval, etc.).
Testing feels slow at first. It isn't — it shifts the slowness to the moment you're writing the test, so you never feel it during a production incident at 2 AM. That's the trade worth making.
