Skip to main content
Version: 4.9.0

Implementing Tests Guide

Create ServiceNow Automated Test Framework (ATF) test cases using Fluent APIs across 11 ATF categories: server, form, REST, catalog, email, app navigator, reporting, responsive dashboard, and Service Portal variants (form_SP, catalog_SP). This guide covers test strategy, category selection, step configuration, and the full API surface for each ATF namespace.

When to Use

  • Generating automated test cases for a ServiceNow application
  • Testing forms, APIs, catalog items, dashboards, or server-side logic
  • Building end-to-end workflow tests combining multiple ATF categories
  • Validating email notifications, report visibility, or navigation menus

Instructions

Strategic Approach

  1. Analyze the application context -- examine custom tables, forms, APIs, catalog items, dashboards, and business logic.
  2. Develop a test strategy -- propose up to 3 representative test cases reflecting critical workflows before expanding coverage.
  3. Select ATF categories -- map each test interaction to the appropriate atf.* namespace (see table below).
  4. Implement test steps using the category-specific Fluent ATF APIs.

Cover every piece of custom logic. If the app has any custom logic, it gets a test -- however simple the app looks. Custom logic includes business rules, client scripts, UI policies, data policies, ACLs/roles, field-level conditions (mandatory / visible / read-only), default / calculated / derived values, validation, and choice / state transitions or side-effects (notifications, related-record updates, flows). Where a rule has both sides, cover a positive AND a negative case. Only when the app genuinely has no custom logic at all -- a plain table with default platform behavior -- does a single basic CRUD test suffice.

Editing an existing app -- reconcile, don't just add. If the task changed or removed existing logic (not only added new logic), bring the existing ATF tests back in sync BEFORE authoring any new ones. Sort the logic this task touched into three buckets and act per bucket:

  • Added logic -- ADD a test (positive AND negative where the rule has both sides).
  • Changed logic -- the rule / UI policy / notification still exists but its behavior changed -- UPDATE its existing test in place. Do NOT delete-and-re-add; editing the existing test IS the update.
  • Removed logic -- the rule / UI policy / notification no longer exists after this edit -- DELETE its test. Query the obsolete test's sys_id (runQuery on sys_atf_test), then emit Now.del('sys_atf_test', '<sysId>') as a top-level statement and re-install. Do NOT rely on just deleting the .now.ts source file -- in a configuration project (build-agent apps default to this) removing source does NOT prune the already-installed record; the build only warns "delete them manually". Now.del() is what removes it; also delete the dead source file so the test isn't re-authored. Re-query after install to confirm the record (and any lingering sys_atf_test_step children) is gone.

The delete-vs-update decision hinges on one question per affected rule: does the rule still exist after my edit? If yes -- update the test in place. If no -- delete it. Delete a test ONLY when its underlying rule is gone; if the behavior merely changed, update, do not delete. Anchor "removed" to what you actually did (you deleted the rule's source / the rule's record is gone after this install), not a guess.

Deleting the test for removed logic is as mandatory as adding a test for new logic -- a test that still asserts deleted or changed behavior is a defect, and adding new tests does not excuse leaving stale ones behind.

Verify before you finish. If (and only if) this task removed any logic, confirm that no surviving test still asserts a removed rule: walk your "removed" bucket and check each maps to zero remaining tests. If the task removed nothing, there is nothing to check -- do not go hunting.

Cover a rule's full trigger set. When a rule fires on more than one database operation (e.g. both insert and update), exercise each path, not just one: an insert case AND a separate update case. Use atf.server.recordInsert for the insert path and atf.server.recordUpdate for the update path, each followed by its own atf.server.recordValidation.

Category Selection

Interaction typeATF namespaceUse for
UI navigationatf.applicationNavigatorVerify menus/modules visible, navigate to modules
Form interactionsatf.formOpen/submit forms, set/validate fields, click UI actions
Forms in Service Portalatf.form_SPSame as form but in Service Portal context
REST API validationatf.restSend HTTP requests, assert status codes/headers/payload
Server-side logicatf.serverImpersonation, CRUD operations, record validation, logging
Service Catalogatf.catalogOpen/order catalog items, set/validate variables
Catalog in Service Portalatf.catalog_SPSame as catalog but in portal -- plus order guides and multi-row variable sets
Email testingatf.emailValidate outbound emails, generate inbound emails
Reportingatf.reportingAssert report visibility
Dashboardsatf.responsiveDashboardAssert dashboard visibility and sharing

Test File Structure

Every ATF test file must:

import { Test } from "@servicenow/sdk/core";
import "@servicenow/sdk/global";

Test({
$id: Now.ID['test_id'],
name: 'test name',
description: 'meaningful description -- see "Writing the Test Description"',
failOnServerError: true
}, (atf) => {
// Steps execute sequentially
atf.<category>.<method>({
$id: Now.ID['step_id'],
...params
});
});
  • $id must be globally unique for both the test and each step.
  • Steps execute sequentially -- capture earlier step outputs in variables to pass to later steps.

Writing the Test Description

Every Test() should set a description that makes the test self-explanatory to someone who never saw the app. State two things:

  • What it validates -- name the specific behavior: the business rule, data policy, UI policy, ACL, field condition (mandatory/visible/read-only), calculated/default value, or state transition. Be concrete -- name the field, rule, table, and triggering condition, not "validates the form".
  • How it validates it -- the mechanism: the data the test sets up, the action it performs, and the assertion that proves the behavior. For a both-sides condition, say which side this case covers (condition holds vs. does not hold).

Write 1-2 sentences, tied to the assertion the test actually makes so the description and steps cannot drift apart.

Examples:

  • ✅ "Inserts an incident with state=7 (Closed) and an assignment group, then asserts it is excluded from the daily-digest query -- the negative side of the digest's state NOT IN 6,7 filter."
  • ✅ "Sets Priority=1 on the Change form and asserts Justification becomes mandatory (condition-holds side of the UI policy); the paired case with Priority=3 asserts it stays optional."
  • ❌ "Tests the incident form." / "Validates the business rule." (neither what specifically nor how)

Category Selection Guidance

  • Prefer UI-based categories (atf.form, atf.catalog) over atf.server for interactions that users normally perform through the UI.
  • Use atf.server only when backend assertions, data setup, or server-only operations are needed.
  • Testing a Script Include (or other server-side-only logic): use atf.server.runServerSideScript. There is no UI for a Script Include, so instantiate the class and invoke its methods inside the script, then assert the result server-side (e.g., throw on mismatch, or run Jasmine specs). Do NOT try to exercise a Script Include through atf.form/atf.catalog.
  • When the user mentions Service Portal, use the _SP variants (atf.form_SP, atf.catalog_SP).
  • Combine categories within a single test for end-to-end workflows.

Key Concepts

  • Test data setup: Use atf.server.impersonate and atf.server.createUser to establish user context. Use atf.server.recordInsert to create prerequisite data.
  • Assertion chaining: After atf.form.submitForm, follow with atf.server.recordValidation to verify the record was created correctly server-side.
  • Form UI flavors: standard_ui, or the sys_id of a sys_ux_page_registry record for any other workspace.
  • Navigator styles: ui15, ui16, polaris.
  • Catalog variable format: IO:<sys_id>=<value> joined with ^ and ending with ^EQ.
  • Encoded queries: Field value conditions use ServiceNow encoded query syntax (e.g., short_description=Test^priority=1).

API Reference: atf.server

Methods

MethodDescriptionKey Output
impersonateImpersonate a user for the test{ user }
createUserCreate a user with roles and groups{ user }
logLog a message to test resultsvoid
runServerSideScriptRun an arbitrary server-side script -- e.g. exercise a Script Include or run Jasmine specs{ table, record_id }
recordQueryQuery records with encoded query{ table, first_record }
recordInsertInsert a record{ table, record_id }
recordValidationValidate record meets conditionsvoid
recordUpdateUpdate a record's fieldsvoid
recordDeleteDelete a recordvoid
searchForCatalogItemSearch catalog items{ catalog_item_id }
checkoutShoppingCartCheckout cart{ request_id }
replayRequestItemReplay a previous request item{ table, req_item }

impersonate

NameTypeMandatoryDescription
userstring | Record<'sys_user'>YesUser to impersonate

createUser

NameTypeMandatoryDescription
firstNamestringYesFirst name
lastNamestringYesLast name
fieldValuesPartial<Data<'sys_user'>>YesAdditional user fields (JSON)
groupsArray<string>YesGroup sys_ids
rolesArray<string>YesRole sys_ids
impersonatebooleanYesWhether to impersonate after creation

runServerSideScript

Runs a script on the server. This is the step to use when testing a Script Include or any server-side-only logic that has no UI. Supports custom assertions (outputs, stepResult, assertEqual) and Jasmine test suites (describe/it/expect).

NameTypeMandatoryDescription
scriptstringNoServer-side script to run. Instantiate the Script Include and assert its output here.
jasmineVersionstringNoJasmine version to use. Currently only '3.1' is supported (default; may change in future platform releases).

Available in script context:

  • outputs — Set output variables for later steps
  • steps(SYS_ID) — Retrieve output variables from earlier steps
  • params — Access parameterized test data
  • stepResult.setOutputMessage(msg) — Log message to step results
  • assertEqual(assertion) — Compare assertion.shouldbe vs assertion.value
  • Return true/false to pass/fail step (ignored by Jasmine tests)
import { Test } from '@servicenow/sdk/core';
import '@servicenow/sdk/global';

// Example 1: Script Include with custom assertion
Test({
$id: Now.ID['script_include_test'],
name: 'Script Include compute test',
description: 'Validates MyScriptInclude.compute() using a custom assertion',
}, (atf) => {
atf.server.runServerSideScript({
$id: Now.ID['test_script_include'],
jasmineVersion: '3.1', // Current version; may change in future
script: `
(function(outputs, steps, params, stepResult, assertEqual) {
var helper = new MyScriptInclude();
var result = helper.compute(2, 3);
assertEqual({ name: 'compute result', shouldbe: 5, value: result });
outputs.computed_value = result;
stepResult.setOutputMessage('Script Include test passed');
})(outputs, steps, params, stepResult, assertEqual);
`,
});
});

// Example 2: Jasmine test suite
Test({
$id: Now.ID['jasmine_suite_test'],
name: 'MyScriptInclude Jasmine suite',
description: 'Runs a Jasmine describe/it suite against MyScriptInclude',
}, (atf) => {
atf.server.runServerSideScript({
$id: Now.ID['jasmine_suite'],
jasmineVersion: '3.1',
script: `
(function(outputs, steps, params, stepResult, assertEqual) {
describe('MyScriptInclude', function() {
it('should compute sum correctly', function() {
var helper = new MyScriptInclude();
expect(helper.compute(2, 3)).toBe(5);
});
});
})(outputs, steps, params, stepResult, assertEqual);
jasmine.getEnv().execute();
`,
});
});

recordInsert / recordUpdate

NameTypeMandatoryDescription
tableTableNameYesTarget table
fieldValuesPartial<Data<T>>YesField-value map (snake_case keys)
assertstringNo'record_successfully_inserted' / 'record_not_inserted' / 'record_successfully_updated' / 'record_not_updated'
enforceSecuritybooleanNoDefault: true
recordIdstringYes (update only)sys_id of record to update

recordValidation

NameTypeMandatoryDescription
tableTableNameYesTable to validate against
recordIdstringYessys_id of record
fieldValuesstringYesEncoded query condition
assertstringNo'record_validated' / 'record_not_found'

API Reference: atf.form

Methods

openNewForm, openExistingRecord, submitForm, setFieldValue, fieldValueValidation, fieldStateValidation, uiActionVisibility, clickUIAction, clickModalButton, declarativeActionVisibility, clickDeclarativeAction

Key Properties

openNewForm: table (required), view, formUI (default: "standard_ui")

setFieldValue: table (required), fieldValues (required, JSON object), formUI

submitForm: assert ("", "form_submitted_to_server", "form_submission_canceled_in_browser"), formUI. Returns { table, record_id }.

fieldValueValidation: table, conditions (encoded query), formUI

fieldStateValidation: table, visible[], notVisible[], readOnly[], notReadOnly[], mandatory[], notMandatory[], formUI

clickUIAction: table, uiAction (sys_id), assert, actionType ("ui_action" or "declarative_action"), formUI

API Reference: atf.rest

Methods

sendRestRequest, assertStatusCodeName, assertStatusCode, assertResponseTime, assertResponseHeader, assertResponsePayload, assertResponseJSONPayloadIsValid, assertJsonResponsePayloadElement, assertResponseXMLPayloadIsWellFormed, assertXMLResponsePayloadElement

sendRestRequest

NameTypeMandatoryDescription
pathstringYesAPI path (e.g., /api/now/table/incident)
bodystringYesJSON string request body
authstringYes'basic', 'mutual', or ''
methodstringNo'get', 'post', 'put', 'delete', 'patch'
queryParametersobjectNoKey-value query params
headersobjectNoKey-value request headers

Assert Methods

  • assertStatusCode: statusCode (number), operation ('equals', 'not_equals', 'less_than', etc.)
  • assertResponsePayload: responseBody (string), operation ('contains', 'equals', etc.)
  • assertJsonResponsePayloadElement: elementName (JSON path), elementValue, operation

API Reference: atf.catalog

Methods

openCatalogItem, addItemToShoppingCart, setCatalogItemQuantity, orderCatalogItem, validatePriceAndRecurringPrice, validateVariableValue, variableStateValidation, setVariableValue, openRecordProducer, submitRecordProducer

Key Properties

openCatalogItem: catalogItem (sys_id, required)

setVariableValue: catalogItem (sys_id), variableValues (format: IO:<sys_id>=<value>^IO:<sys_id>=<value>^EQ)

orderCatalogItem: assert ('form_submitted_to_server' or 'form_submission_cancelled_in_browser'). Returns { request_id, cart }.

Important sequencing: openCatalogItem must precede orderCatalogItem. openRecordProducer must precede submitRecordProducer.

API Reference: atf.email

Methods

MethodDescription
validateOutboundEmailFilter sys_email table for sent emails
validateOutboundEmailGeneratedByNotificationFilter by notification source
validateOutboundEmailGeneratedByFlowFilter by flow source
generateInboundEmailGenerate a new inbound email
generateInboundReplyEmailGenerate an inbound reply
generateRandomStringGenerate test data string

generateInboundEmail

from, to, subject, body (all required strings). Returns { output_email_record }.

API Reference: atf.applicationNavigator

Methods

  • moduleVisibility: Check if modules are visible in navigation. navigator ('ui15', 'ui16', 'polaris'), visibleModules[], notVisibleModules[].
  • navigateToModule: Navigate to a module. module (sys_id).
  • applicationMenuVisibility: Check if app menus are visible. visible[], notVisible[].

API Reference: atf.reporting

reportVisibility

report (sys_id of sys_report), assert ('can_view_report' or 'cannot_view_report').

API Reference: atf.responsiveDashboard

responsiveDashboardVisibility

dashboard (sys_id of pa_dashboards), assert ('dashboard_is_visible' or 'dashboard_is_not_visible').

responsiveDashboardSharing

dashboard (sys_id), assert ('can_share_dashboard' or 'cannot_share_dashboard').

Service Portal Variants

atf.form_SP

Same methods as atf.form with additional portal and page properties plus openServicePortalPage method. Uses form_SP namespace.

atf.catalog_SP

Same methods as atf.catalog with additional portal and page properties, plus: openOrderGuide, navigatewithinOrderGuide, validateOrderGuideItem, reviewOrderGuideSummary, saveCurrentRowOfMultiRowVariableSet, addRowToMultiRowVariableSet.

Avoidance

  1. Do not overuse atf.server for tasks that form or catalog APIs handle directly.
  2. Do not hardcode sys_id values -- always look them up.
  3. Do not skip mandatory fields when using setFieldValue or recordInsert.
  4. Do not call sequence-dependent steps out of order.
  5. Do not create generic or template-based tests -- each test should reflect real usage scenarios.
  6. Do not use workspace name strings (e.g. service_operations_workspace) for formUI -- they 404. Use the workspace's sys_ux_page_registry sys_id.

Example: End-to-End Form Test

import "@servicenow/sdk/global";
import { Test } from "@servicenow/sdk/core";

Test({
$id: Now.ID["validate_incident_form"],
name: "Create and Validate Incident",
description: "Opens a new incident form, sets fields, submits, and validates",
failOnServerError: true
}, (atf) => {
atf.form.openNewForm({
$id: Now.ID["open_new_incident"],
table: "incident",
formUI: "standard_ui"
});

atf.form.setFieldValue({
$id: Now.ID["set_fields"],
table: "incident",
fieldValues: {
short_description: "Email server is down"
},
formUI: "standard_ui"
});

const result = atf.form.submitForm({
$id: Now.ID["submit_form"],
assert: "form_submitted_to_server",
formUI: "standard_ui"
});

atf.server.recordValidation({
$id: Now.ID["validate_record"],
table: "incident",
recordId: result.record_id,
fieldValues: "short_description=Email server is down",
assert: "record_validated"
});
});

Example: REST API Test

import "@servicenow/sdk/global";
import { Test } from "@servicenow/sdk/core";

Test({
$id: Now.ID["scaffold_api_test"],
name: "Scaffold API Test",
failOnServerError: true
}, (atf) => {
atf.rest.sendRestRequest({
$id: Now.ID["send_request"],
path: "/api/now/fluent/scaffold",
body: "",
auth: "basic",
method: "get",
queryParameters: { new: "true" },
headers: {}
});

atf.rest.assertStatusCode({
$id: Now.ID["assert_status"],
operation: "equals",
statusCode: 200
});

atf.rest.assertResponseJSONPayloadIsValid({
$id: Now.ID["assert_json_valid"]
});

atf.rest.assertJsonResponsePayloadElement({
$id: Now.ID["assert_result"],
elementName: "result",
operation: "equals",
elementValue: "success"
});
});