Testing Arcjet
Unlike network-based security solutions, Arcjet can run locally, in staging, and in production. This helps you test your security rules before deploying them.
You can also automate testing against your Arcjet-protected routes using standard testing libraries.
If you are testing AI tool calls or other non-HTTP actions, use the
Agent guards test matrix
and the in-memory test client. Guard tests must assert that a denied or
unavailable decision prevents the side effect, not only that Arcjet returned a
particular result. Capture tests record that an allowed action happened, so they
must assert that the application recorded the expected action after the side
effect ran. See Capture events.
Test with Newman
Section titled “Test with Newman”Newman is an open source command-line collection runner for Postman. It lets you define requests in Postman format and run them from the command line without requiring Postman itself.
You can find our full example of how to test Arcjet with Express and Newman on GitHub. The key files are:
index.jsis a small Express server that uses Arcjet to protect several routes. This is the application being tested.tests/api.test.jsis the test runner which loads the test definitions, executes them with Newman, and then asserts the expected results.- The
tests/bots.json,tests/high-rate-limit.json, andtests/low-rate-limit.jsonfiles define the test requests in Postman Collection format.
The example
README
explains how to start the test application and then execute the tests using
Newman.
To adapt these to your own application you would modify the tests/*.json files
to point to your application endpoints, and then run them using the Newman CLI.
This lets you automate testing and run multiple iterations, for example to test a
rate limit.
Trigger each rule in a test
Section titled “Trigger each rule in a test”Arcjet runs the same in production as it does locally, so the behavior you see in development matches production. You can trigger each rule in the following ways:
- Shield: Send 5 requests with the special header
x-arcjet-suspicious: trueto trigger the shield rule on the next request. - Rate limiting: Make more requests than the rate limit allows.
- Bot protection: Bot detection uses multiple heuristics, so the most
reliable way to get a bot detection
DENYresponse is to make a request that is always considered a bot. With a deny rule set toCURLyou get aDENYresponse when you make a request using thecurlcommand. For more information, see Identifying bots. - Email validation: Use an email address that has invalid syntax or does not match any other rules you have configured, such as an address with no MX records or one from a disposable email service.
Test guard and capture calls
Section titled “Test guard and capture calls”HTTP collection runners cannot reach guard() or capture(). Register an
in-memory test client so that you can assert against application code that
imports the free functions without talking to Arcjet.
Passing a client explicitly is the recommended production path.
launchArcjet() / launch_arcjet() never registers one. Registration is a
separate call for code too deep to receive a handle.
Free guard() fail-opens if no client is registered. It returns ALLOW
with an error result, so hasFailedOpen() / has_failed_open() is true.
That is “policy did not run”, not a pass. Free capture() drops the event
silently.
import { registerTestClient } from "@arcjet/guard/testing";import { refund } from "./refund.ts";
test("refund captures an event", async () => { using arcjet = registerTestClient();
await refund("inv_1");
assert.equal(arcjet.captures[0]?.action, "refund.issued");});using unregisters at the end of the block, including when the test fails.
The using syntax needs Node.js 24 or a TypeScript compile step. On Node.js
22, call unregister() from a finally:
test("refund captures an event", async () => { const arcjet = registerTestClient(); try { await refund("inv_1"); assert.equal(arcjet.captures[0]?.action, "refund.issued"); } finally { arcjet.unregister(); }});registerTestClient() throws if a client is already registered, which
surfaces a leak from an earlier test. Each recorded capture goes through the
same validation as a real capture(), so a call the real client would drop is
not recorded. Recording is synchronous – no flush() is required.
guard() on the test client records the call and returns a fail-open ALLOW,
because no rule ran. It does not stub per-rule verdicts. Helpers that fail
closed on a failed-open decision – guardTool, guardAction – therefore
deny against this client.
To register a real client in production:
import { launchArcjet, registerArcjet } from "@arcjet/guard";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });registerArcjet(arcjet);Free guard(), capture(), and flush() then reach that client. A second
registration does not displace the first.
from arcjet.guard.testing import register_test_client
async def test_refund_captures_an_event(): with register_test_client() as arcjet: await refund("inv_1") assert arcjet.captures[0].action == "refund.issued"Usually this belongs in a fixture:
import pytestfrom arcjet.guard.testing import register_test_client
@pytest.fixturedef arcjet(): with register_test_client() as client: yield clientregister_test_client() raises if a client is already registered. The test
client answers both guard() and guard_sync() and returns a fail-open
ALLOW. Recording is synchronous – no flush() is required.
To register a real client in production:
from arcjet.guard import launch_arcjet, register_arcjet
register_arcjet(launch_arcjet(key=os.environ["ARCJET_KEY"]))capture() is one function for both client flavors. guard() / flush()
must match the registered client: await guard(...) with launch_arcjet(),
or guard_sync(...) with launch_arcjet_sync(). The wrong pair fail-opens
and reports AJ3007.
For registration edge cases and the policy-denial matrix, see Agent guards testing and reference.
Sampling
Section titled “Sampling”To write a sampling function that tests your Arcjet security rules on a subset of your traffic, see the sampling blueprint.