← Documentation

Form Tests

Professional & Enterprise

A form with thirty rules is hard to change with confidence. Adjust one rule and the honest way to check the other twenty-nine is to open the form and click through it — which is slow enough that in practice nobody does it, and regressions get found by the people using the form.

The Tests tab, beside Rules in the form configuration, lets you write down what the form is supposed to do and re-run those checks whenever you want. A test is: set these values, then check that these things are true.

Running tests never touches real data. No email is sent, nothing is written to another list, no Flow or webhook fires, no message box appears, and the item is never saved. This is the guarantee the feature rests on — section 5 sets out exactly what it covers. Custom JavaScript is never run by a test in the configuration; a test run on the real form does call your functions, under the conditions set out in section 11.

The Tests tab in the form configuration: a line saying these tests belong to the new form, a note that tests never touch real data, a Run all tests button reading 2 passed and 1 failed, a Run these tests before saving the configuration toggle switched off, and three test rows — Approving requires an approver and Assigning an approver queues the notification with green ticks, Rejecting notifies the requester with a red cross — above Record a test and Add a test buttons.

1. Writing a test

Open the form configuration, pick the Tests tab, then Add a test. A test has three parts.

PartWhat it is
NameWhat should be true, written as a sentence — for example Approving requires an approver. This is what you read in the results list, so a name that states the expectation beats one that describes the steps.
Set these valuesThe field values applied before the checks run. They are applied in order, the same way a user filling in the form would enter them, and rules react to each one as it is set.
Then check thatOne or more checks. Every check must hold for the test to pass.
A test open for editing: the Name box reads Approving requires an approver; under Set these values, a field picker set to Status and a choice picker set to Approved; under Then check that, a check of type A field is required or optional targeting Approver with its toggle set to is required; and a Pin the date section switched off, above Save and Cancel.

How values are entered

Each value is entered with whatever control the column justifies, rather than with a text box for everything:

Column typeWhat you get
Choice and multi-choiceThe column’s own choices. A test cannot fail because of a typo in a value that was never valid to begin with.
Yes/NoA toggle, which stores a real boolean.
PersonA text box that accepts an email address, a login name, a user id or a group name. Dynamic content tokens such as [[currentuser:…]] are resolved first.
Everything elseA text box. Lookups and people resolve against SharePoint, so their valid values cannot be listed in the configuration.

Why the Yes/No toggle rather than a text box. A Yes/No field treats anything it does not recognize as No. Typed into a box, “yes” is not one of the values it accepts — so the test would set the field to No, the rule under test would be checked against the wrong value, and nothing on screen would say so. The toggle stores a real boolean, so there is nothing to get wrong.

2. The checks you can make

CheckWhat it asserts
A rule fires / does not fireThe named rule matched, or did not.
A field is shown / is hiddenField visibility after the rules have run. A field hidden with the Hide element action or with Custom CSS counts as hidden, even though it is still on the form; a field on a tab the user has not selected counts as shown.
A field is required / is optionalWhether the field is in the required set.
A field is read-only / is editableWhether the field can be edited.
A field has a valueFour comparisons — equals, is empty, is not empty, and is the current user. See section 2a. Equals is a loose comparison: 5000 matches "5000", so a number field does not fail on its type.
A tab is shown / is hiddenTab visibility after the rules have run.
An HTML section or element is shown / is hiddenAny element the Show element / Hide element actions can reach — an HTML section, a custom row, a row, a column. Pick it from the list of the form’s content elements and everything your rules can toggle, or type an id, or a CSS class starting with a dot. A check written this way survives the rule behind it being renamed, split in two or reordered.
Validation passes / Validation failsWhether the form would pass its required-field check.
A rule runs an actionThat a rule would have run a given action — that it would send its email, for instance. The action is recorded, not run.

2a. Checking a person field

Equality is rarely the check you want on a person field. If a rule fills in an approver, what you care about is that the rule filled the approver in — not that the rule filled in Bob, which stops being true the moment the approver changes. So A field has a value offers four comparisons:

ComparisonWhen to use it
equalsThe default — an exact match against the value you supply.
is emptyThe field was left blank — or a rule cleared it.
is not emptySomeone was filled in, without naming them. Usually the right check for a person a rule assigns.
is the current userThe field holds whoever is running the test, compared against the real signed-in user — the same source [[currentuser:…]] resolves from.

This is what recording proposes for you. When a rule assigns a person field from [[currentuser:loginName]], the recorded test would end up holding your own account — and a check written as equals “your.name@company.com” would pass for you and fail for every colleague, which is the kind of bug a test suite should catch rather than create. So recording proposes is the current user instead.

If the signed-in user cannot be determined, is the current user fails and says so, rather than passing on a comparison that was never actually made. Note that a test’s pinned date and user profile are not consulted by this check — those stand in for [[userprofile:…]] and group conditions, which are a different question.

3. Recording a test instead of writing one

Writing a test from a blank page means describing behaviour you would rather just demonstrate. Press Record a test and DFFS moves you to the form preview and starts watching. Fill the form in the way a user would, then press Stop and review.

You are shown what changed: the values you entered, and a checklist of what the form did in response. Untick anything you do not want checked, give the test a name, and save.

What is not suggested matters as much as what is:

  • A value a rule worked out for you is ticked. A value you typed is offered unticked — checking the input you just entered proves nothing about the form.
  • Only things that actually changed are offered. A form has dozens of fields that sat still, and a check for each would bury the handful that matter.
  • Required and read-only are never proposed for a field that ended up hidden, because that check fails a hidden field outright and would hand you a test that is broken the moment it is created.
The Review the recorded test panel. Under Set these values: Status = Approved and Approver = email@example.com. Under Then check that, a checklist with Rule Ready fires, Rule Approved fires, Rule Notify the requester on rejection does not fire, Rule Email new approver fires, Field Requester is the current user, and Field Approver is required all ticked — while Field Approver is the current user and Field Status equals Approved are offered unticked. Below, an empty Name box with Save and Discard recording buttons.

Notice what is not ticked in that list: Field “Status” equals “Approved” and Field “Approver” is the current user are both offered unticked, because Status and Approver are the two values that were typed in. Field “Requester” is the current user is ticked — a rule worked that one out.

If nothing changed while you were recording, DFFS says so rather than saving an empty test — and if nothing changed in response to your values, it says that too, since that may itself be the problem you were looking for.

Recording runs in test mode as well, so filling in the form to record a test cannot send a real email either.

4. Reading a failure

Results are a plain green/red list — passed, failed, or could not run. Expanding a failure shows the check that broke and, underneath it, a rule condition table like the one Rule Debug produces, naming the exact condition and the value that was actually there:

An expanded failing test named Rejecting notifies the requester. The check reads: Rule Notify the requester on rejection runs its sendEmail action. Below it in red: Expected — but the rule did not match, so the action was never reached. Under a Conditions heading, a table with one row: Type Row, Trigger field:Status, Operator eq, Expected Rejected, Actual Pending, Match crossed.

The failure line above it says why in words — the rule did not match, so the action was never reached — and the table says which condition was responsible: the rule wanted Status to be Rejected and actually found Pending. If you have used Debug this rule, this table will be familiar — it is the same one.

The result also lists Actions the test intercepted, using the same action names as the rule builder. That is how you confirm a rule would have sent its notification. Actions taken by the form’s own load rules — a message box shown when the form opens, say — happen in every test, so they are listed separately under Intercepted while the form loaded rather than as something this test caused.

5. What a test can and cannot touch

Six rule actions reach outside the form. During a test they are recorded, never executed:

  • Send email — nothing is queued and nothing is sent.
  • Write to another list — no item is created or updated.
  • Trigger a Flow and call a webhook — nothing is called.
  • Show a message box — no dialog interrupts the run.
  • Run custom JavaScript — your function is not called. On the real form this one is opt-in per run; see section 11.

The remaining rule actions are in-memory or affect only the display, and they run normally — so the form reacts during a test exactly as it would for a user.

Expanding a test that passed shows what was recorded instead of run, under Actions the test intercepted — the rule, the action, and the action’s own configuration:

An expanded passing test named Assigning an approver queues the notification, which ran in 2003 ms, followed by Every check passed. Under an Actions the test intercepted heading: Email new approver, arrow, Send email — and beneath it the email action's stored configuration as JSON, showing its recipient token, subject and body. None of it was sent.

That is the whole guarantee in one panel: the test confirmed the notification would go out, the configuration shows exactly what it would have contained, and nobody received it.

The item is never saved. Test values are applied in memory, and the form is returned to its starting state when the run finishes — including the data a save would write. That last part matters in practice: it means running a test and then pressing Save cannot save test values by accident.

6. Rules that call your custom JavaScript

A rule can decide whether it matches by calling a function — the run based on the result of a Custom JS function condition. A test run from the Tests tab never runs your function. Instead you tell the test what it should return, and the editor lists only the function names your form’s rules actually reference.

This is deliberately better than running the real thing:

  • The test stays predictable. Your function may call SharePoint, read the clock, or check who is signed in. Running it would make the test’s result depend on things the test does not control.
  • Both answers become testable. You can write returns Yes → approver required and returns No → not required. Running the real function only ever tests the answer it gives today.

A function you leave unset returns No, and the result says so under Custom JS conditions with no result set — shown on passes as well as failures, because a test passing for that reason is exactly the kind of silent problem worth surfacing. On the real form an unset function is called for real instead, and the result names the functions that ran — see section 11. A result you pin here always wins, in both places, which is what keeps a test that covers both branches working wherever it is run.

Rules triggered from Custom JS with dffs_triggerRule need nothing set up. They only fire when your code calls them, so not firing during a test is the correct behaviour.

7. Tests that depend on the date

A rule or formula that compares against today gives a different answer next month, which makes for a test that passes now and fails later for no real reason. A test can pin the date it was written for, in yyyy-mm-dd format. That freezes today’s date everywhere the form uses it — the [today] column default, =today+N default formulas, the “today” operand of a calculated field, the [[timestamp:…]] token, relative date restrictions, and TODAY() / NOW() in formulas.

It is off by default, and that is the right default. A test with a pinned date can never catch a problem that only appears on a later date. Pin it only when the rule you are testing depends on the date.

8. Running the tests before a save

Run these tests before saving the configuration is an option per form. The enabled tests run before anything is written, so choosing not to save leaves you exactly where you were.

Failures warn, they do not block. Part way through a change you will legitimately have failing tests, and a hard block would only teach people to switch the option off. A progress dialog shows how many have run and offers two clear choices — skip the rest and save, or stop and do not save — because “cancel” is ambiguous when you were the one who pressed Save. The run stops between tests, never mid-test.

Individual tests can be left out of a run with Skip in Run all, and put back with Include in Run all.

The configuration cannot be saved while a test is running or a recording is open. Save shows Finish the test first instead: stop the recording, or let the test finish, and then save. A test run puts the rules in the state a running form would have them in, and saving in the middle of one could store that state with the configuration. Run these tests before saving is not affected — it starts its run after you press Save.

9. Tests that point at a deleted rule

A rule that has been deleted and a rule that simply did not fire look identical from the outside — but the fix is the opposite in each case. One means repair the test; the other means look at the rule.

A check against a rule the form no longer has says this test refers to a rule that is no longer on this form, and the Tests tab flags affected tests before you run them. Rules are matched on name as well as id, so renaming a rule — or recreating one by hand — does not break the test that refers to it.

From 2.6, deleting a rule that tests depend on asks first and lists those tests, so this mostly catches rules deleted before then, or by importing a configuration.

10. Where tests live

New, Display and Edit are separate configurations with their own rules, so a form’s tests belong to that form. There is no form selector; the tab states which form its tests apply to, and a test that arrives from somewhere else — through a copied configuration — is refused with a clear message rather than being run against a different set of rules.

Because tests are stored with the configuration, they travel with it: they are covered by the unsaved-changes prompt, included in export and import, and promoted through DEV → UAT → PRD along with everything else.

11. Running the tests on the real form

The Tests tab answers “did I break rule #7?” in seconds while you are editing rules. It cannot answer “does this work for a real user?”, because the configuration’s preview never runs your Custom JS. So the same tests can also be run against the live form.

Add ?dffs_tests=1 to the form’s URL (use &dffs_tests=1 if the URL already has a ? in it) and a Form tests button appears beside Rule Debug, for anyone who can manage the list. dffs_showFormTests() in the browser console does the same without editing the URL.

The form really reacts — values are set and rules run — and your Custom JS runs too. A rule condition that calls a function calls the real one and uses its answer, unless the test pins a result for that function. The result lists the functions that were called, so you can see which answers came from your own code rather than from the test.

The safety rules still hold, and they are enforced rather than assumed:

  • The item is never saved. Save, Ctrl+S, dffs_triggerSave and the autosave timer are all refused while a run is in progress, no draft is written, and the form is returned to its starting values afterwards. Every rule then runs once on those values, so the form looks as it did before the run — for example, a field a rule shows only for the test’s values is hidden again. Email, webhooks and flows that already ran when the form opened are not sent a second time.
  • The six outward-reaching actions are still recorded, not carried out — exactly as in the configuration.
  • Custom JS actions are opt-in per run. A checkbox in the panel, off by default and not remembered between runs. A condition function returns an answer; an action function is meant to do something to a real item, which is not an implicit opt-in.
  • dffs_spAdd, dffs_spUpdate and dffs_spDelete refuse to write while a test is running, and the refusal is listed on the result. A condition function whose write was refused is treated as No and the rest of the rule still evaluates. If your condition function writes to SharePoint, pin its result on the test case — that skips calling it at all, and keeps the test working in both places.

This removes a sharp edge; it is not a sandbox. Nothing can stop Custom JS that calls PnP or fetch directly — running your own code is the point of a run on the real form.

Two differences to plan for. A run on the real form sees only the saved configuration, so a rule change has to be saved before you can test it there. And the run starts from the item that is open, so any field a test does not set itself carries that item’s value — the same test can pass on one item and fail on another. That is fidelity gained and reproducibility lost, which is the trade the two places make in opposite directions.

Tests cannot be started from the pill while a vLookup child form is open — a run would drive the child form and report the result as the parent’s, so it refuses instead.

12. Tests and rules together

From 2.6, the Tests tab and the Rules tab know about each other. A test is not stored against a rule — one test can cover several rules, and one rule can be covered by several tests — so the link is worked out from what the test checks, at two strengths:

  • Checks this rule — the test has a rule fires or runs an action check on the rule. Exact.
  • Checks what this rule does — the test checks something the rule’s actions change: a field it makes required, visible or read-only, a field it sets, an element it shows or hides, a tab it toggles. A strong hint rather than proof, since another rule may be the one the test is really about. Validation checks are never linked.

In the Rules tab

A rule with linked tests shows an N tests pill in its header. Its menu lists the linked tests under those two headings — click one to open the Tests tab on it — and Run these tests runs just those, then opens the Tests tab on the first failure, or on the first test if all passed. Deleting a rule that tests depend on asks first and lists them.

In the Tests tab

FeatureWhat it does
NotesWhy the test exists, what it protects — anything a later reader should know. The first line shows under the test name, and a failing test’s note is printed under the failure in the browser console.
Saved resultsEach test keeps its last result between sessions, shown as a lighter ✓ or ✗, with the time of the run on hover. The result is saved with the configuration; running tests does not on its own count as an unsaved change.
Rule changedShown when a rule the test is linked to has been edited since the test last ran, so the saved result may be out of date. Run the test again.
DuplicateA copy of the test, placed under the original and opened for editing, with no saved result.
Copy to paste into another formNew, Edit and Display each have their own tests. Copy a test here, open another form’s Tests tab, and a Paste button appears there. A check on a rule that form does not have is flagged as described in section 9. The × beside Paste discards the copy without pasting it.
N rules have no testsLists the enabled rules no test is linked to. Shown once the form has at least one test.

Editing a test opens the editor in place of that test, rather than below the whole list.

Create a rule from this test

Write the test first, then let it draft the rule. Create a rule from this test builds a rule that would make the test pass:

  • The condition is one field equals value row per value the test sets — or Form load if it sets none. A value holding several items is written as text, and you are told to check it.
  • The actions come from the checks:
Check in the testAction in the drafted rule
A field is required / optionalRequired fields / Optional fields
A field is shown / hiddenVisible fields / Hidden fields
A field is read-only / editableRead-only fields / Editable fields
An HTML section or element is shown / hiddenShow elements / Hide elements
A tab is shown / hiddenShow/hide tabs
A field equals a value, is empty, or is the current userSet field value

Checks with no matching action — validation, rule fires, runs an action, a field is not empty — are left out and listed. When the condition is on a field, the rule’s No branch gets the opposite actions, so it undoes itself when the condition stops matching; a Form load rule always matches, so it gets none.

The draft is added at the end of the rules, opened in the Rules tab for you to review, and the test gains a rule fires check on it — so from then on the two are linked exactly.

Tests do not re-run by themselves when you edit a rule. A run switches the configurator to the form preview, which would pull you out of the Rules tab in the middle of an edit. The Rule changed marker and the pill’s Run these tests are there instead.

Practical notes

  • Name a test after the expectation, not the steps. Approving requires an approver tells you what broke when it goes red; Set status and check field does not.
  • Record first, then trim. Recording proposes checks you would not think to write, and unticking is faster than composing.
  • Test both branches of a Custom JS condition. Two tests — one for Yes, one for No — cover a rule that a single test with the real function never could.
  • Rules that check Entra ID group membership are reported, not guessed, in the configurator. A run from the Tests tab never calls Microsoft Graph, so it cannot know which Entra ID groups the user is in. A rule that depends on one is listed on the result as a condition that could not be evaluated, rather than reading as a rule that did not fire. Run the tests on the real form to exercise the real membership lookup.
  • Prefer is not empty over equals on a person field. A test naming a specific person breaks when that person changes role, and a test recorded against your own account breaks for everyone else. Check that the rule filled someone in, not who.
  • Write a test when you fix a rule, not only when you build one. A test written at the moment you understand a bug is the one that stops it coming back.
  • A test asserting that a rule would send its email is the safe way to check notification logic. The check is on the action, and nobody receives anything.
  • Turn on “run before saving” once your tests are trustworthy. Doing it while they are still half-written produces warnings you learn to dismiss.