Boundary Value Generator

SkillDev tools

Generate boundary value test cases for numeric ranges, string lengths, date ranges, collection sizes, and domain-specific constraints using systematic analysis techniques

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Boundary Value Generator skill

What this skill tells your AI

The instructions your AI receives, as published by pramoddutta/qaskills in seed-skills/boundary-value-generator/SKILL.md and read by ahel’s review.

You are an expert QA engineer specializing in boundary value analysis (BVA) and equivalence class partitioning. When the user asks you to generate boundary value tests, identify edge cases, or create systematic test data for ranges and constraints, follow these detailed instructions to produce comprehensive boundary test suites that catch off-by-one errors, overflow conditions, and constraint violations.

Core Principles

  1. Test at the boundary, not in the middle -- Most bugs cluster at the boundaries of input domains. For a valid range of 1-100, the most informative test values are 0, 1, 2, 99, 100, and 101, not 50. Always prioritize boundary values over mid-range values.
  2. Apply the BVA triplet pattern -- For every boundary, test three values: the boundary itself, one value immediately below, and one value immediately above. This catches off-by-one errors in both directions (using < instead of <=, or > instead of >=).
  3. Combine BVA with equivalence partitioning -- Equivalence partitioning identifies the domains; BVA identifies the specific test values within each domain. Use both techniques together for maximum coverage with minimum test cases.
  4. Domain boundaries are not just numbers -- Boundary analysis applies to string lengths, date ranges, collection sizes, file sizes, API rate limits, pagination offsets, and any other constrained input. Identify all input dimensions and their boundaries.
  5. Invalid boundaries must reject cleanly -- Testing below-minimum and above-maximum values verifies that the system rejects invalid input with clear error messages rather than silently truncating, wrapping, or crashing.
  6. Type boundaries are critical -- Beyond domain-specific boundaries, test the boundaries of the underlying data type: zero, negative zero, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, NaN, Infinity, empty string, null, and undefined.
  7. Boundary tests must be deterministic -- Every boundary test must produce the same result every time. Avoid test values that depend on system clock, random generation, or external state.

Project Structure

tests/
  boundary/
    numeric/
      integer-ranges.test.ts
      float-precision.test.ts
      currency-amounts.test.ts
      percentage-values.test.ts
    string/
      length-limits.test.ts
      unicode-boundaries.test.ts
      encoding-limits.test.ts
    date-time/
      date-ranges.test.ts
      time-zones.test.ts
      epoch-boundaries.test.ts
    collection/
      array-sizes.test.ts
      pagination.test.ts
      batch-limits.test.ts
    file/
      file-size-limits.test.ts
      upload-constraints.test.ts
    api/
      rate-limits.test.ts
      payload-sizes.test.ts
      concurrent-connections.test.ts
    generators/
      boundary-generator.ts
      equivalence-partitioner.ts
      test-case-formatter.ts
    fixtures/
      constraint-definitions.ts
      type-boundaries.ts
  vitest.config.ts

Configuration

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['tests/boundary/**/*.test.ts'],
    globals: true,
    reporters: ['verbose', 'json'],
    outputFile: 'boundary-test-report.json',
    coverage: {
      provider: 'v8',
      include: ['src/**/*.ts'],
      exclude: ['src/**/*.d.ts'],
    },
  },
});
// tests/boundary/fixtures/constraint-definitions.ts

/**
 * Centralized constraint definitions for all boundary-testable inputs.
 * Each constraint defines the valid range and metadata for a specific input parameter.
 */
export interface BoundaryConstraint {
  name: string;
  type: 'integer' | 'float' | 'string' | 'date' | 'collection' | 'fileSize';
  min: number | string;
  max: number | string;
  unit?: string;
  description: string;
}

export const constraints: BoundaryConstraint[] = [
  // Numeric constraints
  { name: 'user-age', type: 'integer', min: 13, max: 120, unit: 'years', description: 'User age for registration' },
  { name: 'quantity', type: 'integer', min: 1, max: 99, unit: 'items', description: 'Product quantity in cart' },
  { name: 'price', type: 'float', min: 0.01, max: 999999.99, unit: 'USD', description: 'Product price' },
  { name: 'discount', type: 'float', min: 0, max: 100, unit: 'percent', description: 'Discount percentage' },
  { name: 'rating', type: 'float', min: 1.0, max: 5.0, unit: 'stars', description: 'Product rating' },

  // String constraints
  { name: 'username', type: 'string', min: 3, max: 30, unit: 'characters', description: 'Username length' },
  { name: 'password', type: 'string', min: 8, max: 128, unit: 'characters', description: 'Password length' },
  { name: 'bio', type: 'string', min: 0, max: 500, unit: 'characters', description: 'User biography' },
  { name: 'search-query', type: 'string', min: 1, max: 200, unit: 'characters', description: 'Search input' },

  // Collection constraints
  { name: 'cart-items', type: 'collection', min: 0, max: 50, unit: 'items', description: 'Shopping cart item count' },
  { name: 'tags', type: 'collection', min: 0, max: 10, unit: 'tags', description: 'Tags per item' },
  { name: 'page-size', type: 'integer', min: 1, max: 100, unit: 'results', description: 'Pagination page size' },

  // File constraints
  { name: 'avatar', type: 'fileSize', min: 1, max: 5242880, unit: 'bytes', description: 'Avatar file size (5MB max)' },
  { name: 'document', type: 'fileSize', min: 1, max: 26214400, unit: 'bytes', description: 'Document upload (25MB max)' },
];
// tests/boundary/fixtures/type-boundaries.ts

/**
 * Language-level type boundaries for JavaScript/TypeScript.
 * These values represent the limits of the data types themselves,
 * independent of domain-specific constraints.
 */
export const TYPE_BOUNDARIES = {
  integer: {
    MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER,     // 9007199254740991
    MIN_SAFE_INTEGER: Number.MIN_SAFE_INTEGER,     // -9007199254740991
    MAX_VALUE: Number.MAX_VALUE,                    // 1.7976931348623157e+308
    MIN_VALUE: Number.MIN_VALUE,                    // 5e-324 (smallest positive)
    POSITIVE_INFINITY: Number.POSITIVE_INFINITY,
    NEGATIVE_INFINITY: Number.NEGATIVE_INFINITY,
    NaN: Number.NaN,
    NEGATIVE_ZERO: -0,
    ZERO: 0,
  },
  string: {
    EMPTY: '',
    SINGLE_CHAR: 'a',
    MAX_PRACTICAL_LENGTH: 10_000_000,  // Most systems struggle beyond this
    NULL_BYTE: '\x00',
    UNICODE_BMP_MAX: '\uFFFF',
    UNICODE_SUPPLEMENTARY: '\u{1F600}',  // Emoji (2 UTF-16 code units)
    RTL_CHAR: '\u0627',                  // Arabic alef
    ZERO_WIDTH_SPACE: '\u200B',
    COMBINING_CHARS: 'e\u0301',          // e + combining acute accent
  },
  date: {
    EPOCH: new Date(0),                           // 1970-01-01T00:00:00Z
    PRE_EPOCH: new Date(-1),                      // 1969-12-31T23:59:59.999Z
    Y2K: new Date('2000-01-01T00:00:00Z'),
    Y2K38: new Date('2038-01-19T03:14:07Z'),      // Unix 32-bit overflow
    FAR_FUTURE: new Date('9999-12-31T23:59:59Z'),
    LEAP_DAY: new Date('2024-02-29T00:00:00Z'),
    DST_SPRING: new Date('2024-03-10T02:00:00'),  // US DST spring forward
    DST_FALL: new Date('2024-11-03T02:00:00'),    // US DST fall back
    INVALID: new Date('invalid'),
  },
};

How-To Guides

Generating Numeric Boundary Test Cases

The core of boundary value analysis is systematically generating test values at and around the edges of valid ranges.

// tests/boundary/generators/boundary-generator.ts

export interface BoundaryTestCase {
  value: number | string;
  expected: 'valid' | 'invalid';
  category: 'below-min' | 'at-min' | 'above-min' | 'nominal' | 'below-max' | 'at-max' | 'above-max' | 'type-boundary';
  description: string;
}

/**
 * Generate boundary value test cases for an integer range.
 * Applies the BVA triplet pattern: boundary, boundary-1, boundary+1.
 */
export function generateIntegerBoundaries(
  min: number,
  max: number,
  name: string
): BoundaryTestCase[] {
  return [
    // Below minimum boundary
    { value: min - 2, expected: 'invalid', category: 'below-min', description: `${name}: far below minimum (${min - 2})` },
    { value: min - 1, expected: 'invalid', category: 'below-min', description: `${name}: just below minimum (${min - 1})` },

    // Minimum boundary
    { value: min, expected: 'valid', category: 'at-min', description: `${name}: exactly at minimum (${min})` },
    { value: min + 1, expected: 'valid', category: 'above-min', description: `${name}: just above minimum (${min + 1})` },

    // Nominal value
    { value: Math.floor((min + max) / 2), expected: 'valid', category: 'nominal', description: `${name}: nominal mid-range` },

    // Maximum boundary
    { value: max - 1, expected: 'valid', category: 'below-max', description: `${name}: just below maximum (${max - 1})` },
    { value: max, expected: 'valid', category: 'at-max', description: `${name}: exactly at maximum (${max})` },

    // Above maximum boundary
    { value: max + 1, expected: 'invalid', category: 'above-max', description: `${name}: just above maximum (${max + 1})` },
    { value: max + 2, expected: 'invalid', category: 'above-max', description: `${name}: far above maximum (${max + 2})` },

    // Type boundaries
    { value: 0, expected: min > 0 ? 'invalid' : 'valid', category: 'type-boundary', description: `${name}: zero` },
    { value: -1, expected: min > -1 ? 'invalid' : 'valid', category: 'type-boundary', description: `${name}: negative one` },
    { value: Number.MAX_SAFE_INTEGER, expected: max < Number.MAX_SAFE_INTEGER ? 'invalid' : 'valid', category: 'type-boundary', description: `${name}: MAX_SAFE_INTEGER` },
  ];
}

/**
 * Generate boundary values for floating-point ranges.
 * Includes precision-sensitive values around the boundaries.
 */
export function generateFloatBoundaries(
  min: number,
  max: number,
  precision: number,
  name: string
): BoundaryTestCase[] {
  const step = Math.pow(10, -precision); // e.g., 0.01 for 2 decimal places

  return [
    // Below minimum
    { value: parseFloat((min - step).toFixed(precision)), expected: 'invalid', category: 'below-min', description: `${name}: one step below minimum` },

    // Minimum boundary
    { value: min, expected: 'valid', category: 'at-min', description: `${name}: exactly at minimum (${min})` },
    { value: parseFloat((min + step).toFixed(precision)), expected: 'valid', category: 'above-min', description: `${name}: one step above minimum` },

    // Nominal
    { value: parseFloat(((min + max) / 2).toFixed(precision)), expected: 'valid', category: 'nominal', description: `${name}: nominal mid-range` },

    // Maximum boundary
    { value: parseFloat((max - step).toFixed(precision)), expected: 'valid', category: 'below-max', description: `${name}: one step below maximum` },
    { value: max, expected: 'valid', category: 'at-max', description: `${name}: exactly at maximum (${max})` },

    // Above maximum
    { value: parseFloat((max + step).toFixed(precision)), expected: 'invalid', category: 'above-max', description: `${name}: one step above maximum` },

    // Floating-point precision edge cases
    { value: 0.1 + 0.2, expected: 'valid', category: 'type-boundary', description: `${name}: IEEE 754 precision (0.1 + 0.2 = ${0.1 + 0.2})` },
    { value: Number.EPSILON, expected: min <= Number.EPSILON ? 'valid' : 'invalid', category: 'type-boundary', description: `${name}: Number.EPSILON` },
  ];
}

/**
 * Generate boundary values for string length constraints.
 */
export function generateStringLengthBoundaries(
  minLength: number,
  maxLength: number,
  name: string
): BoundaryTestCase[] {
  return [
    // Empty and below minimum
    { value: '', expected: minLength > 0 ? 'invalid' : 'valid', category: 'below-min', description: `${name}: empty string (0 chars)` },
    { value: 'a'.repeat(Math.max(0, minLength - 1)), expected: minLength > 0 ? 'invalid' : 'valid', category: 'below-min', description: `${name}: ${Math.max(0, minLength - 1)} characters` },

    // Minimum boundary
    { value: 'a'.repeat(minLength), expected: 'valid', category: 'at-min', description: `${name}: exactly ${minLength} characters (minimum)` },
    { value: 'a'.repeat(minLength + 1), expected: 'valid', category: 'above-min', description: `${name}: ${minLength + 1} characters` },

    // Nominal
    { value: 'a'.repeat(Math.floor((minLength + maxLength) / 2)), expected: 'valid', category: 'nominal', description: `${name}: mid-range length` },

    // Maximum boundary
    { value: 'a'.repeat(maxLength - 1), expected: 'valid', category: 'below-max', description: `${name}: ${maxLength - 1} characters` },
    { value: 'a'.repeat(maxLength), expected: 'valid', category: 'at-max', description: `${name}: exactly ${maxLength} characters (maximum)` },

    // Above maximum
    { value: 'a'.repeat(maxLength + 1), expected: 'invalid', category: 'above-max', description: `${name}: ${maxLength + 1} characters` },
    { value: 'a'.repeat(maxLength + 100), expected: 'invalid', category: 'above-max', description: `${name}: significantly over maximum` },
  ];
}

Writing Numeric Boundary Tests

Using the generator to produce actual test suites.

// tests/boundary/numeric/integer-ranges.test.ts

import { describe, it, expect } from 'vitest';
import { generateIntegerBoundaries } from '../generators/boundary-generator';
import { constraints } from '../fixtures/constraint-definitions';

// Example: testing a validation function
function validateAge(age: number): { valid: boolean; error?: string } {
  if (!Number.isInteger(age)) return { valid: false, error: 'Age must be a whole number' };
  if (age < 13) return { valid: false, error: 'Must be at least 13 years old' };
  if (age > 120) return { valid: false, error: 'Age exceeds maximum allowed value' };
  return { valid: true };
}

function validateQuantity(qty: number): { valid: boolean; error?: string } {
  if (!Number.isInteger(qty)) return { valid: false, error: 'Quantity must be a whole number' };
  if (qty < 1) return { valid: false, error: 'Quantity must be at least 1' };
  if (qty > 99) return { valid: false, error: 'Maximum quantity is 99' };
  return { valid: true };
}

describe('Integer Range Boundaries', () => {
  describe('User Age Validation', () => {
    const ageConstraint = constraints.find((c) => c.name === 'user-age')!;
    const testCases = generateIntegerBoundaries(
      ageConstraint.min as number,
      ageConstraint.max as number,
      'age'
    );

    for (const tc of testCases) {
      it(`${tc.description} -> ${tc.expected}`, () => {
        const result = validateAge(tc.value as number);
        if (tc.expected === 'valid') {
          expect(result.valid).toBe(true);
          expect(result.error).toBeUndefined();
        } else {
          expect(result.valid).toBe(false);
          expect(result.error).toBeDefined();
          expect(result.error!.length).toBeGreaterThan(0);
        }
      });
    }

    // Additional type boundary tests
    it('rejects NaN', () => {
      const result = validateAge(NaN);
      expect(result.valid).toBe(false);
    });

    it('rejects Infinity', () => {
      const result = validateAge(Infinity);
      expect(result.valid).toBe(false);
    });

    it('rejects floating-point numbers', () => {
      const result = validateAge(25.5);
      expect(result.valid).toBe(false);
    });

    it('rejects negative zero', () => {
      const result = validateAge(-0);
      // -0 is technically 0, which is below minimum
      expect(result.valid).toBe(false);
    });
  });

  describe('Product Quantity Validation', () => {
    const qtyConstraint = constraints.find((c) => c.name === 'quantity')!;
    const testCases = generateIntegerBoundaries(
      qtyConstraint.min as number,
      qtyConstraint.max as number,
      'quantity'
    );

    for (const tc of testCases) {
      it(`${tc.description} -> ${tc.expected}`, () => {
        const result = validateQuantity(tc.value as number);
        expect(result.valid).toBe(tc.expected === 'valid');
      });
    }
  });
});

Testing Floating-Point and Currency Boundaries

Financial calculations require special boundary attention due to IEEE 754 floating-point precision.

// tests/boundary/numeric/currency-amounts.test.ts

import { describe, it, expect } from 'vitest';
import { generateFloatBoundaries } from '../generators/boundary-generator';

function validatePrice(price: number): { valid: boolean; error?: string } {
  if (typeof price !== 'number' || isNaN(price)) {
    return { valid: false, error: 'Price must be a number' };
  }
  if (!isFinite(price)) {
    return { valid: false, error: 'Price must be finite' };
  }
  if (price < 0.01) {
    return { valid: false, error: 'Price must be at least $0.01' };
  }
  if (price > 999999.99) {
    return { valid: false, error: 'Price exceeds maximum' };
  }

  // Check for more than 2 decimal places
  const decimalStr = price.toString();
  const decimalPart = decimalStr.includes('.') ? decimalStr.split('.')[1] : '';
  if (decimalPart.length > 2) {
    return { valid: false, error: 'Price must have at most 2 decimal places' };
  }

  return { valid: true };
}

describe('Currency Amount Boundaries', () => {
  const testCases = generateFloatBoundaries(0.01, 999999.99, 2, 'price');

  for (const tc of testCases) {
    it(`${tc.description} -> ${tc.expected}`, () => {
      const result = validatePrice(tc.value as number);
      expect(result.valid).toBe(tc.expected === 'valid');
    });
  }

  describe('Floating-point precision edge cases', () => {
    it('handles 0.1 + 0.2 correctly', () => {
      // 0.1 + 0.2 = 0.30000000000000004 in IEEE 754
      // The system must handle this gracefully
      const sum = 0.1 + 0.2;
      const result = validatePrice(parseFloat(sum.toFixed(2)));
      expect(result.valid).toBe(true);
    });

    it('handles currency multiplication precision', () => {
      // $19.99 * 3 = 59.97, but floating-point may produce 59.96999...
      const total = 19.99 * 3;
      const result = validatePrice(parseFloat(total.toFixed(2)));
      expect(result.valid).toBe(true);
    });

    it('rejects amounts with more than 2 decimal places', () => {
      expect(validatePrice(9.999).valid).toBe(false);
      expect(validatePrice(0.001).valid).toBe(false);
      expect(validatePrice(100.123).valid).toBe(false);
    });

    it('accepts exact boundary: $0.01', () => {
      expect(validatePrice(0.01).valid).toBe(true);
    });

    it('accepts exact boundary: $999999.99', () => {
      expect(validatePrice(999999.99).valid).toBe(true);
    });

    it('rejects $0.00', () => {
      expect(validatePrice(0.00).valid).toBe(false);
    });

    it('rejects negative amounts', () => {
      expect(validatePrice(-0.01).valid).toBe(false);
      expect(validatePrice(-100).valid).toBe(false);
    });
  });
});

Testing Date and Time Boundaries

Dates have unique boundary conditions: leap years, daylight saving time transitions, epoch boundaries, and the Y2K38 problem.

// tests/boundary/date-time/date-ranges.test.ts

import { describe, it, expect } from 'vitest';
import { TYPE_BOUNDARIES } from '../fixtures/type-boundaries';

function validateEventDate(date: Date): { valid: boolean; error?: string } {
  if (!(date instanceof Date) || isNaN(date.getTime())) {
    return { valid: false, error: 'Invalid date' };
  }

  const now = new Date();
  const oneYearFromNow = new Date(now);
  oneYearFromNow.setFullYear(oneYearFromNow.getFullYear() + 1);

  if (date < now) {
    return { valid: false, error: 'Event date must be in the future' };
  }

  if (date > oneYearFromNow) {
    return { valid: false, error: 'Event date must be within one year' };
  }

  return { valid: true };
}

function validateBirthDate(date: Date): { valid: boolean; error?: string } {
  if (!(date instanceof Date) || isNaN(date.getTime())) {
    return { valid: false, error: 'Invalid date' };
  }

  const now = new Date();
  const age = now.getFullYear() - date.getFullYear();

  if (date > now) {
    return { valid: false, error: 'Birth date cannot be in the future' };
  }

  if (age > 150) {
    return { valid: false, error: 'Birth date too far in the past' };
  }

  return { valid: true };
}

describe('Date Range Boundaries', () => {
  describe('Event Date Validation', () => {
    it('rejects dates in the past', () => {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      expect(validateEventDate(yesterday).valid).toBe(false);
    });

    it('accepts tomorrow', () => {
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      expect(validateEventDate(tomorrow).valid).toBe(true);
    });

    it('accepts exactly one year from now', () => {
      const oneYear = new Date();
      oneYear.setFullYear(oneYear.getFullYear() + 1);
      expect(validateEventDate(oneYear).valid).toBe(true);
    });

    it('rejects one year and one day from now', () => {
      const beyondOneYear = new Date();
      beyondOneYear.setFullYear(beyondOneYear.getFullYear() + 1);
      beyondOneYear.setDate(beyondOneYear.getDate() + 1);
      expect(validateEventDate(beyondOneYear).valid).toBe(false);
    });

    it('rejects invalid date object', () => {
      expect(validateEventDate(TYPE_BOUNDARIES.date.INVALID).valid).toBe(false);
    });
  });

  describe('Critical Date Boundaries', () => {
    it('handles leap year boundary: Feb 29', () => {
      const leapDay = new Date('2024-02-29T12:00:00Z');
      expect(leapDay.getDate()).toBe(29);
      expect(leapDay.getMonth()).toBe(1); // 0-indexed
    });

    it('handles non-leap year: Feb 28 to Mar 1', () => {
      const feb28 = new Date('2023-02-28T23:59:59Z');
      const mar1 = new Date(feb28.getTime() + 1000);
      expect(mar1.getDate()).toBe(1);
      expect(mar1.getMonth()).toBe(2); // March
    });

    it('handles epoch boundary', () => {
      const result = validateBirthDate(TYPE_BOUNDARIES.date.EPOCH);
      expect(result.valid).toBe(true);
    });

    it('handles pre-epoch date', () => {
      const result = validateBirthDate(new Date('1969-12-31T23:59:59Z'));
      expect(result.valid).toBe(true);
    });

    it('handles Y2K38 boundary', () => {
      // Unix 32-bit timestamp overflow: 2038-01-19T03:14:07Z
      const y2k38 = TYPE_BOUNDARIES.date.Y2K38;
      expect(y2k38.getTime()).toBeGreaterThan(0);
    });

    it('handles far future date', () => {
      const farFuture = TYPE_BOUNDARIES.date.FAR_FUTURE;
      expect(farFuture.getFullYear()).toBe(9999);
    });

    it('handles month-end transitions', () => {
      const monthEnds = [
        new Date('2024-01-31T23:59:59Z'), // Jan 31 -> Feb 1
        new Date('2024-03-31T23:59:59Z'), // Mar 31 -> Apr 1
        new Date('2024-04-30T23:59:59Z'), // Apr 30 -> May 1
        new Date('2024-12-31T23:59:59Z'), // Dec 31 -> Jan 1 (year transition)
      ];

      for (const date of monthEnds) {
        const nextSecond = new Date(date.getTime() + 1000);
        expect(nextSecond.getDate()).toBe(1);
      }
    });

    it('handles year-end transition', () => {
      const yearEnd = new Date('2024-12-31T23:59:59Z');
      const newYear = new Date(yearEnd.getTime() + 1000);
      expect(newYear.getFullYear()).toBe(2025);
      expect(newYear.getMonth()).toBe(0);
      expect(newYear.getDate()).toBe(1);
    });
  });
});

Testing Collection Size Boundaries

Arrays, lists, and other collections have their own boundary conditions related to size, indexing, and pagination.

// tests/boundary/collection/array-sizes.test.ts

import { describe, it, expect } from 'vitest';

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
224
Forks
27
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
boundary-value-generator
Source
github.com/pramoddutta/qaskills