Scripting GuideValue Generators

Value Generator Functions

Value Generators allow you to create dynamic, reusable data for your tests. They’re a key part of ReAPI’s empowerment model: developers write complex generation logic once, and QA uses it everywhere via simple function calls.

What Are Value Generators?

Value Generators are functions that:

  • Generate dynamic test data (IDs, timestamps, complex objects)
  • Fetch data from external sources (test data APIs)
  • Transform data based on parameters
  • Encapsulate complex data creation logic

Key advantages:

  • Reusable - Write once, call hundreds of times
  • Parameterized - Flexible configuration via function parameters
  • UI-accessible - QA can configure and use via no-code UI
  • Testable - Built-in testing capability before activation

Two Ways to Use Generators

QA can use generators directly in the UI without writing any code:

  • Select generator from dropdown
  • Configure parameters via form fields
  • Preview generated value
  • Use in test node

2. JavaScript Expressions (For Dynamic Scenarios)

Call generators from JS expressions for more flexibility:

// In expression field:
js: $gen.generateUserId();
js: $gen.createTestUser({ role: "admin", verified: true });
js: $gen.calculateOrderTotal($context.order, { includeShipping: true });

Script Pattern

A Value Generator is a JavaScript function that returns a value. The function can accept zero or one parameter.

// No parameter
function getCurrentTimestamp() {
  return Date.now();
}
 
// Simple parameter (e.g., a number)
async function generateUsers(count) {
  const users = [];
  for (let i = 0; i < count; i++) {
    users.push({
      id: i + 1,
      name: `User ${i + 1}`,
    });
  }
  return users;
}
 
// Complex parameter (e.g., an object)
async function generateTestData(options) {
  const { type, count = 10 } = options || {};
 
  if (type === "users") {
    return Array.from({ length: count }, (_, i) => ({
      id: i + 1,
      name: `User ${i + 1}`,
    }));
  }
 
  return { message: "Unknown type" };
}

Key Points

  • Function Name: Can be any valid JavaScript function name (not fixed like hooks)
    • Important: The function name must match the title you give it in the UI
    • Example: UI title “Generate Test User” → function name generateTestUser
  • Parameters: The function can accept zero or one parameter
    • No parameter: function getTimestamp()
    • Single primitive: function createUsers(count)
    • Single object: function createUser(options) ← Recommended for complex parameters
  • Return Value: The function must return a value (can be any type)
  • Async/Sync: The function can be either async or synchronous
  • Context Access: You can read from $context if needed
  • Parameter Validation: Use Zod to validate and parse parameters

Parameter Examples

// Called with a primitive parameter: 5
async function createUsers(count) {
  // count will be 5
  return Array.from({ length: count }, () => ({
    /* ... */
  }));
}
 
// Called with an object parameter: { type: 'admin', count: 3 }
async function createUsers(options) {
  // options will be { type: 'admin', count: 3 }
  const { type, count } = options;
  return Array.from({ length: count }, () => ({ type /* ... */ }));
}
 
// Called with no parameter
function getDefaultConfig() {
  return { timeout: 30000, retries: 3 };
}

Parameter Validation with Zod

Best practice: Always validate complex parameters with Zod to ensure type safety and provide clear error messages.

Why Validate Parameters?

  • Type safety - Catch invalid parameters early
  • Default values - Automatically apply defaults
  • Clear errors - Zod provides descriptive error messages
  • Self-documenting - Schema serves as parameter documentation

Pattern: Validate with parse()

Use Zod’s parse() method to validate and throw errors automatically:

import { z } from "zod";
 
// Define schema
const UserSchema = z.object({
  role: z.enum(["admin", "user", "guest"]).default("user"),
  verified: z.boolean().default(false),
  count: z.number().int().positive().default(1),
});
 
async function generateTestUsers(options) {
  // Validate and parse parameters (throws if invalid)
  const config = UserSchema.parse(options || {});
 
  const users = [];
  for (let i = 0; i < config.count; i++) {
    users.push({
      id: faker.number.int({ min: 1000, max: 9999 }),
      email: faker.internet.email(),
      role: config.role,
      verified: config.verified,
      createdAt: Date.now(),
    });
  }
 
  return users;
}

What happens:

  • Valid parameters: Function continues with validated data
  • Invalid parameters: Zod throws descriptive error, UI shows it to user
  • Missing parameters: Zod applies defaults automatically

Example: Complex Generator with Validation

import { z } from "zod";
 
// Define reusable schema in Global Scripts for consistency
class $$GeneratorSchemas {
  static tenantMetrics = z.object({
    count: z
      .number()
      .int()
      .positive()
      .default(10)
      .describe("Number of metrics to generate"),
    min: z.number().default(0).describe("Minimum metric value"),
    max: z.number().default(1000).describe("Maximum metric value"),
    unit: z.string().default("requests").describe("Metric unit"),
    includeHash: z
      .boolean()
      .default(true)
      .describe("Include data integrity hash"),
  });
}
 
async function generateTenantMetrics(options) {
  // Validate parameters
  const config = $$GeneratorSchemas.tenantMetrics.parse(options || {});
 
  const metrics = [];
  for (let i = 0; i < config.count; i++) {
    const value = _.random(config.min, config.max);
    const metric = {
      id: faker.string.uuid(),
      value,
      unit: config.unit,
      timestamp: Date.now(),
      tenantId: $context.tenantId,
    };
 
    if (config.includeHash) {
      metric.hash = $$AnalyticsUtils.generateDataHash(metric);
    }
 
    metrics.push(metric);
  }
 
  return metrics;
}

Common Patterns

Pattern 1: Simple Data Generation

// No parameter - generate unique ID
function generateUserId() {
  return `user_${Date.now()}_${_.random(1000, 9999)}`;
}
 
// No parameter - current timestamp
function getCurrentTimestamp() {
  return Date.now();
}
 
// No parameter - random test email
function generateTestEmail() {
  return faker.internet.email();
}

Pattern 2: Parameterized Data Generation

import { z } from "zod";
 
const OrderSchema = z.object({
  itemCount: z.number().int().positive().default(3),
  currency: z.string().default("USD"),
  includeShipping: z.boolean().default(true),
});
 
async function generateTestOrder(options) {
  const config = OrderSchema.parse(options || {});
 
  const items = [];
  let subtotal = 0;
 
  for (let i = 0; i < config.itemCount; i++) {
    const price = _.random(10, 200);
    const quantity = _.random(1, 5);
    items.push({
      id: faker.string.uuid(),
      name: faker.commerce.productName(),
      price,
      quantity,
    });
    subtotal += price * quantity;
  }
 
  const shipping = config.includeShipping ? 10 : 0;
 
  return {
    id: faker.string.uuid(),
    items,
    subtotal,
    shipping,
    total: subtotal + shipping,
    currency: config.currency,
    createdAt: Date.now(),
  };
}

Pattern 3: Fetch Remote Data

import { z } from "zod";
 
const UserCriteriaSchema = z.object({
  role: z.enum(["admin", "user", "guest"]).optional(),
  verified: z.boolean().optional(),
  country: z.string().optional(),
});
 
async function fetchTestUser(criteria) {
  const config = UserCriteriaSchema.parse(criteria || {});
 
  // Build query parameters
  const params = new URLSearchParams();
  if (config.role) params.append("role", config.role);
  if (config.verified !== undefined) params.append("verified", config.verified);
  if (config.country) params.append("country", config.country);
 
  // Fetch from test data API
  const baseUrl =
    $context.config.testDataApiUrl || "https://test-data.example.com";
  const response = await ky
    .get(`${baseUrl}/users?${params}`, {
      timeout: 10000,
    })
    .json();
 
  return response.data;
}

Pattern 4: Transform Existing Data

import { z } from "zod";
 
const TransformSchema = z.object({
  includeShipping: z.boolean().default(true),
  taxRate: z.number().min(0).max(1).default(0.2),
  discountPercent: z.number().min(0).max(100).default(0),
});
 
async function calculateOrderTotal(order, options) {
  const config = TransformSchema.parse(options || {});
 
  // Calculate subtotal
  const subtotal = order.items.reduce((sum, item) => {
    return sum + item.price * item.quantity;
  }, 0);
 
  // Apply discount
  const discount = subtotal * (config.discountPercent / 100);
  const afterDiscount = subtotal - discount;
 
  // Apply tax
  const tax = afterDiscount * config.taxRate;
 
  // Add shipping
  const shipping = config.includeShipping ? order.shipping || 10 : 0;
 
  return {
    subtotal,
    discount,
    tax,
    shipping,
    total: afterDiscount + tax + shipping,
  };
}

Pattern 5: Leverage Global Utilities

import { z } from "zod";
 
const MetricSchema = z.object({
  value: z.number(),
  unit: z.string(),
  slaThreshold: z.number().optional(),
});
 
async function generateValidatedMetric(options) {
  const config = MetricSchema.parse(options || {});
 
  const metric = {
    id: faker.string.uuid(),
    value: config.value,
    unit: config.unit,
    timestamp: Date.now(),
    formattedTime: $$AnalyticsUtils.formatTimestamp(Date.now()),
    hash: $$AnalyticsUtils.generateDataHash({ value: config.value }),
  };
 
  // Use global utility for validation
  if (config.slaThreshold) {
    metric.withinSLA = $$AnalyticsUtils.isWithinSLA(
      config.value,
      config.slaThreshold
    );
  }
 
  return metric;
}

Best Practices

1. Always Validate Complex Parameters

// ✅ Good: Validated parameters with defaults
import { z } from "zod";
 
const Schema = z.object({
  count: z.number().int().positive().default(10),
  type: z.enum(["user", "admin"]).default("user"),
});
 
async function generateUsers(options) {
  const config = Schema.parse(options || {});
  // ... use validated config
}
 
// ❌ Bad: No validation, runtime errors likely
async function generateUsers(options) {
  const count = options.count; // Could be undefined, string, negative, etc.
  // ... likely to fail
}

2. Use Object Parameters for Flexibility

// ✅ Good: Flexible, extensible
async function createUser(options) {
  const config = UserSchema.parse(options || {});
  // Can add more options later without breaking existing calls
}
 
// ❌ Bad: Hard to extend
async function createUser(name, email, age, role, verified, country) {
  // Adding more parameters breaks all existing calls
}

3. Provide Sensible Defaults

import { z } from "zod";
 
const Schema = z.object({
  count: z.number().int().positive().default(10), // Default to 10
  includeMetadata: z.boolean().default(true), // Default to true
  format: z.enum(["json", "xml"]).default("json"), // Default to json
});

4. Use Descriptive Function Names

// ✅ Good: Clear intent
async function generateTestUserWithOrders() {
  /* ... */
}
async function fetchValidatedPaymentData() {
  /* ... */
}
async function calculateTotalWithTax() {
  /* ... */
}
 
// ❌ Bad: Ambiguous
async function generate() {
  /* ... */
}
async function getData() {
  /* ... */
}
async function calc() {
  /* ... */
}

5. Test Before Activating

ReAPI provides built-in testing for generators:

  1. Write your function
  2. Use the UI testing feature to test with different parameters
  3. Verify the output is correct
  4. Only then activate for QA use

Important: Once activated and used in tests, avoid disabling—update instead or create versioned functions.


Key Takeaways

  1. Function Name Must Match UI Title - Not fixed like hooks
  2. Single Parameter Only - Use object for complex parameters
  3. Validate with Zod - Use parse() for automatic validation and defaults
  4. Async or Sync - Both supported, use async for HTTP/external calls
  5. Leverage Global Utilities - Call $$Utils for shared logic
  6. Provide Defaults - Make parameters optional with sensible defaults
  7. Test Thoroughly - Use built-in testing before activation
  8. UI-Accessible - QA can use via no-code UI or JS expressions

For comprehensive examples, see Build a Testing Framework.