Skip to main content
Version: Latest (4.10.0)

Implementing Tests Guide

Create ServiceNow Automated Test Framework (ATF) test cases using Fluent APIs across 13 ATF categories: server, form, REST, catalog, email, app navigator, reporting, responsive dashboard, uiTestScript (custom UI), list and related list operations, 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, related lists, 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
Custom UI (UI pages, SPAs, workspace/UIB components, now-* web components)atf.uiTestScriptNot reachable via atf.form/atf.catalog -- see the atf-ui-test-script-guide doc before ruling this out
Lists and related listsatf.listFilter/validate related lists, check record presence, click list UI actions
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.

  • Related lists are ubiquitous by default (Attachments, Approvals, Journal Entries, and any list arising purely from a reference field) -- only add an atf.list test when a client script, UI policy, or a relationship this app defines actually drives that related list's visibility, membership, or filtering, not default platform behavior.

  • Determine this by checking, not assuming. This is how you resolve the point above -- run the three searches below before concluding a related list has no custom logic behind it, rather than skipping straight to "no test needed." Do not treat "the user didn't mention this" as evidence that nothing exists -- pre-existing logic (written before this conversation started) is invisible unless you look. Before ruling atf.list out for a target table, search the three places below. Filter for relevance, don't just dump every record on the table -- these tables carry many OOB/platform rows unrelated to related lists at all; a match only counts if its content or purpose actually touches list/related-list behavior, not merely that it exists on the table. Test whatever you find touching the related list -- do not filter by who authored it or when, or by what scope it lives in; a match is in scope whether it's OOB or custom, and whether it lives in Global or the app currently being developed. A client script, UI policy, or relationship driving this behavior is just as often in Global scope as in this app's own scope -- do not add a scope restriction to these searches.

    • Client script -- sys_script_client on the table, filtered to content, e.g. scriptLIKEhideRelatedList^ORscriptLIKEshowRelatedList^ORscriptLIKEaddRelatedList.
    • UI policy -- query sys_ui_policy with two separate pure-^AND queries (never combine them with ^OR -- see note below): table=<table>^run_scripts=true^script_trueLIKErelatedlist and table=<table>^run_scripts=true^script_falseLIKErelatedlist. relatedlist (case-insensitive) is deliberately the broad substring, not the specific method name -- it catches hideRelatedList/showRelatedList/addRelatedList and any other RelatedList-touching call in one term, confirmed against real live examples on other tables (e.g. "Hide the Time series exclusions related list...", "Show/Hide activities related list..."), so incident returning no matches means this table genuinely has none, not that the search missed something. run_scripts=true matters because script_true/script_false are ignored entirely when it's false. Do not merge the two queries with ^OR: ServiceNow's encoded-query ^OR does not distribute an earlier ^AND term across the whole chain, so run_scripts=true^script_trueLIKEx^ORscript_falseLIKEx silently lets run_scripts=false records back in through the second branch (confirmed by testing this exact pattern against a real instance -- a pure ^AND-only chain scopes correctly, but the moment ^OR enters the string, only the term immediately before it stays scoped). UI Policy Actions themselves can't target a related list at all (no field reference exists for one) -- script_true/script_false running raw GlideForm API calls (the same g_form.hideRelatedList()/showRelatedList() a client script would use) is the only way a UI policy record could actually touch a related list, which is why this is a narrower check than the other two.
    • Relationship this app defines -- sys_relationship where basic_apply_to (or apply_to) is this table, for a relationship record (surfaces as a REL:<sys_id> related list per the Related list value format note below), as opposed to a related list arising from a plain reference field or an OOB platform relationship.

    Those tables are examples of where to look, not the limit of what counts -- any logic that conditionally affects a related list's visibility, membership, or filtering is in scope, however and wherever it's implemented.

  • Before ruling out atf.list for a table, verify you actually ran the searches above. Concluding "nothing affects this table's related lists" without having queried sys_script_client, sys_ui_policy, and sys_relationship for that table is not a determination -- it's an assumption, and is exactly the failure mode this section exists to prevent. If you have not run them, STOP and run them now, even if you're already authoring a different category of test for this app.

  • Map found logic to the best-fit atf.list method, not a default. Once a match is found, re-read the found script's actual behavior against every method in the API Reference: atf.list table below (relatedListVisibility, applyFilterToList, recordPresentInList, openRecordInList, listUIActionVisibility, clickListUIAction) and pick the one(s) that match what the logic actually does -- a hide/show branch maps to relatedListVisibility; logic that filters/narrows a list's records maps to applyFilterToList; logic gating a list UI action maps to listUIActionVisibility/clickListUIAction. Cover every distinct branch/condition the found logic has (e.g. both the true and false side of an if), even though the user's request didn't mention list steps explicitly. Only skip atf.list once the search above comes back empty.

  • Once atf.list is warranted for a related list, consider all 5 read-only methods against it, not just whichever one obviously matches the found logic's branching behavior. A related list worth testing for visibility (relatedListVisibility) or filtering (applyFilterToList) is often also worth testing for membership (recordPresentInList), navigation (openRecordInList), and whether any list-level UI actions are visible on it (listUIActionVisibility) -- none of these need additional logic to be meaningful, just a related list already confirmed to be driven by something found in the sweep. clickListUIAction is excluded from this default consideration since it actually executes the action (real side effects); only add it when there's a specific reason to verify the action's behavior, not just as a default check. Create simple prerequisite data (atf.server.recordInsert) if the child table has no existing records to test presence/navigation against.

  • Enumerate every surviving candidate before writing any test -- a literal list, not a mental note. If the sweep finds more than one item (e.g. a client script AND a relationship), do not stop as soon as the first one becomes a test -- "I wrote a test" is not "I covered everything I found." After authoring, walk that same list again and confirm each entry maps to a test you actually wrote before finishing.

  • Report what the sweep found, not just what you decide to test. State every candidate found and whether you're testing it, as you go, before moving to authoring -- don't reason through this silently and only surface the final test list. Visible findings are what let a check that quietly returns an unexpected empty result get caught immediately, instead of only surfacing after tests are already written and you have to ask what query was actually run.

  • When found logic populates or restricts a related list, prefer validating the underlying data/rule with atf.server.recordValidation on the affected record. Add atf.list only when the list-level UI behavior itself -- visibility, filtering, list UI actions -- not just the record it shows, is what needs verifying.

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).
  • Related list value format: atf.list's relatedList parameter is not a guessable string -- valid values are dynamically generated per-table by the platform, in the format <child_table>.<field> (e.g. task.parent, task_ci.task), not <table>_<table>. Some are relationship-based instead, prefixed REL:<sys_id>. This can't be inferred from types -- before authoring, run this via run_script against the target instance to get real values for a table: var lists = new ATFRelatedListUtil().getRelatedLists('incident'); for (var i in lists) gs.info(lists[i].label + ' => ' + lists[i].value); (must run in Global scope).

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').

API Reference: atf.list

Methods

MethodDescriptionKey Output
relatedListVisibilityCheck related lists are visible/not visible on the current formvoid
applyFilterToListApply a filter to a list, narrowing it to matching records{ first_record }
recordPresentInListCheck a specific record is/isn't present in a listvoid
openRecordInListOpen a specific record from a listvoid
listUIActionVisibilityCheck UI actions are visible/not visible on a listvoid
clickListUIActionClick a UI action on a listvoid

Every method except relatedListVisibility takes listType ('related_list' default, or 'list'), relatedList (which related list/list view -- see Related list value format in Key Concepts), and relatedListTable (the table the list's records live in -- recordId and applyFilterToList's first_record output are typed against this table, not table).

Important sequencing: applyFilterToList is typically used to narrow to a record before recordPresentInList/openRecordInList/clickListUIAction. listUIActionVisibility (can I see the action) generally precedes clickListUIAction (exercise it) when both are being verified.

Worked Example -- All 6 Methods

Demonstrates correct parameter usage for every atf.list method, chained on a real, out-of-the-box related list (incident's "Task -> Parent", task.parent) -- this shows the API mechanics correctly, it is NOT an example of when to use atf.list (see Category Selection Guidance above -- only author these steps when a client script, UI policy, or relationship actually drives the behavior, never for a plain related list with nothing behind it like this one).

import { Test } from '@servicenow/sdk/core'

Test({ $id: Now.ID['list_test'], name: 'ATF List Test' }, (atf) => {
atf.list.relatedListVisibility({
$id: Now.ID['rl_vis'],
table: 'incident',
visible: ['task.parent'],
})

const filterResult = atf.list.applyFilterToList({
$id: Now.ID['apply_filter'],
table: 'incident',
relatedList: 'task.parent',
relatedListTable: 'task',
filterConditions: 'active=true^EQ',
assert: 'records_match_filter',
})

atf.list.recordPresentInList({
$id: Now.ID['record_present'],
table: 'incident',
relatedList: 'task.parent',
relatedListTable: 'task',
recordId: filterResult.first_record,
assert: 'record_present',
})

atf.list.openRecordInList({
$id: Now.ID['open_record'],
table: 'incident',
relatedList: 'task.parent',
relatedListTable: 'task',
recordId: filterResult.first_record,
})

atf.list.listUIActionVisibility({
$id: Now.ID['list_ui_action_visibility'],
table: 'incident',
relatedList: 'task.parent',
relatedListTable: 'task',
visible: ['Update'],
})

atf.list.clickListUIAction({
$id: Now.ID['click_list_ui_action'],
table: 'incident',
relatedList: 'task.parent',
relatedListTable: 'task',
listAction: '<real sys_ui_action sys_id on the child table>',
actionType: 'list_banner_button',
assert: 'page_reloaded_or_redirected',
})
})

Not yet runtime-confirmed. This demonstrates correct parameter shape and chaining, verified against the real serialization/install pipeline when originally built -- it has not been confirmed to actually pass when executed via the ATF test runner. Re-verify if a real gap surfaces here.

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"
});
});