Custom Assertions

Custom assertions encapsulate reusable validation logic for your tests.

Export Pattern

// src/assertions/index.ts
import type { AssertionFunction } from "../types";
 
export const $$AssertionFunctions: AssertionFunction[] = [
  isValidEmail,
  isPositiveNumber,
  matchesSchema,
];

Function Signature

Assertions accept 1, 2, or 3 parameters:

// 1 parameter: value only
async function isValidEmail(value: unknown)
 
// 2 parameters: value and expected
async function isGreaterThan(actual: number, expected: number)
 
// 3 parameters: value, expected, and options
async function isCloseTo(actual: number, expected: number, delta?: number)

Recording Results

Always call $addAssertionResult():

$addAssertionResult({
  passed: boolean,      // Did the assertion pass?
  message: string,      // Human-readable result
  operator: string,     // Assertion name for reports
  leftValue: any,       // Actual value
  rightValue: any,      // Expected value
});

Example: Simple Assertion

export const isValidEmail: AssertionFunction = {
  id: "is-valid-email",
  name: "Is Valid Email",
  description: "Validates email format",
  enabled: true,
  deprecated: false,
  tested: true,
  function: async (value: unknown) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const passed = typeof value === "string" && emailRegex.test(value);
 
    $addAssertionResult({
      passed,
      message: passed
        ? `"${value}" is a valid email`
        : `"${value}" is not a valid email`,
      operator: "isValidEmail",
      leftValue: value,
      rightValue: "valid email format",
    });
  },
};

Example: Zod Schema Assertion

import { z } from "zod";
 
const UserSchema = z.object({
  id: z.number().positive(),
  email: z.string().email(),
  name: z.string().min(1),
  role: z.enum(["admin", "user", "guest"]),
});
 
export const isValidUser: AssertionFunction = {
  id: "is-valid-user",
  name: "Is Valid User",
  description: "Validates user object structure",
  enabled: true,
  deprecated: false,
  tested: true,
  function: async (value: unknown) => {
    const result = UserSchema.safeParse(value);
 
    $addAssertionResult({
      passed: result.success,
      message: result.success
        ? "User object is valid"
        : `Invalid user: ${result.error.errors.map(e => e.message).join(", ")}`,
      operator: "isValidUser",
      leftValue: value,
      rightValue: "valid user schema",
    });
  },
};

Example: Two-Parameter Assertion

export const isWithinRange: AssertionFunction = {
  id: "is-within-range",
  name: "Is Within Range",
  description: "Checks if value is within expected range",
  enabled: true,
  deprecated: false,
  tested: true,
  function: async (value: unknown, range: { min: number; max: number }) => {
    const num = Number(value);
    const passed = !isNaN(num) && num >= range.min && num <= range.max;
 
    $addAssertionResult({
      passed,
      message: passed
        ? `${value} is within range [${range.min}, ${range.max}]`
        : `${value} is outside range [${range.min}, ${range.max}]`,
      operator: "isWithinRange",
      leftValue: value,
      rightValue: range,
    });
  },
};

Best Practices

Use safeParse() Not parse()

// ✅ Good: Returns success/error object
const result = schema.safeParse(value);
if (result.success) { ... }
 
// ❌ Bad: Throws on invalid data
const result = schema.parse(value); // Throws!

Always Wrap in Try-Catch

function: async (value) => {
  try {
    // Your logic
    $addAssertionResult({ passed: true, ... });
  } catch (error) {
    $addAssertionResult({
      passed: false,
      message: `Error: ${error.message}`,
      operator: "myAssertion",
      leftValue: value,
      rightValue: "expected",
    });
  }
}

Provide Clear Messages

// ✅ Good: Specific, actionable
message: `User email "${user.email}" is not in valid format`
 
// ❌ Bad: Vague
message: "Invalid"

Usage in Tests

Once synced and enabled, your assertion appears in:

  • Assert Node → Operator dropdown
  • Script Node → Call directly: MyLib.isValidEmail(value)