一键导入
writing-tests
Conventions to follow when writing or modifying tests in this repo. Applied automatically — not user-invocable.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Conventions to follow when writing or modifying tests in this repo. Applied automatically — not user-invocable.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Native batching framework for destination transformations. Extend BatchDestination to implement per-event transforms with automatic grouping, chunking, and response formatting.
Create the transformation logic for a new VDM V2 object-based destination. Implements record dispatch by object type and action, with batching and optional event-stream support.
Code design conventions — handler maps, dependency management, function extraction. Applied automatically — not user-invocable.
TypeScript-specific conventions — type narrowing, optional chaining, type safety. Applied automatically — not user-invocable.
Create the transformation logic for a new VDM Next audience destination. Implements record processing, identifier hashing, batching, and API request building for audience-based integrations.
Generate detailed integration documentation for a rudder-transformer destination. Use when asked to create docs, README, or documentation for a destination integration.
| name | writing-tests |
| description | Conventions to follow when writing or modifying tests in this repo. Applied automatically — not user-invocable. |
Follow these conventions when writing or modifying tests in this repo.
Never use /.+/ or other catch-all patterns in test assertions. Run the code to find the actual error message and match on a distinctive substring.
// Good
errorMatch: /Unexpected token/;
// Bad
errorMatch: /.+/;
expect(result.valid).toBe(true) already throws on failure — don't wrap the next assertion in if (result.valid). Use 'field' in result && result.field for TypeScript narrowing without a runtime branch.
// Good
expect(result.valid).toBe(true);
expect('recordFields' in result && result.recordFields.sort()).toEqual(expected);
// Bad
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.recordFields.sort()).toEqual(expected);
}
it.each()When two or more test cases share the same assertion structure, group them into an it.each() data table. When a describe block has cases with different assertion shapes, use one it.each() per shape. Keep test data arrays at the top of the describe block for easy scanning.
// Good — cases grouped by assertion shape, each group gets its own it.each()
const validCases = [
{ name: 'objects', template: '$.records.({ "e": .email })', expectedFields: ['email'] },
{ name: 'arrays', template: '$.records.([.email, .phone])', expectedFields: ['email', 'phone'] },
];
const invalidCases = [
{ name: 'spread operator', template: '{ ...$.records }', errorMatch: /spread_expr/ },
{ name: 'bare identifier', template: 'process', errorMatch: /Bare identifiers/ },
];
it.each(validCases)('should accept: $name', ({ template, expectedFields }) => { ... });
it.each(invalidCases)('should reject: $name', ({ template, errorMatch }) => { ... });
// Bad — individual blocks with identical assertion structure
it('should reject spread operator', () => {
const result = validateTemplate('{ ...$.records }');
expect(result.valid).toBe(false);
expect(result.errors?.[0]).toMatch(/spread_expr/);
});
it('should reject variable declarations', () => {
const result = validateTemplate('let x = 1');
expect(result.valid).toBe(false);
expect(result.errors?.[0]).toMatch(/definition_expr/);
});
Thoroughly test the logic a layer owns (e.g., input validation in a controller — cover all invalid shapes). For success/failure paths delegated to another module that has its own tests, one representative case each is enough.
// Good — controller owns Zod validation, so test all 400 shapes;
// one valid + one invalid template case since templateValidator.test.ts covers the rest
const badRequestCases = [
{ name: 'missing field', body: {}, expectedError: 'requestBody: Required' },
{ name: 'empty string', body: { requestBody: '' }, expectedError: '...' },
{ name: 'wrong type', body: { requestBody: 123 }, expectedError: '...' },
];
it.each(badRequestCases)('should return 400: $name', async ({ body, expectedError }) => { ... });
it('should return valid=true for a valid template', async () => { ... });
it('should return valid=false for an invalid template', async () => { ... });
// Bad — controller test re-tests every template validation scenario
// that templateValidator.test.ts already covers
it('should reject spread operator', ...);
it('should reject bare identifiers', ...);
it('should reject bracket notation', ...);
Test files use 1:1 name mapping to the source file. If a __tests__/ directory already exists at that level, place the test there. Otherwise, co-locate it alongside the source file.
# If __tests__/ exists
src/controllers/__tests__/eventTest.test.ts
# If no __tests__/ directory
src/v0/destinations/custom_audience/templateValidator.test.ts
All test data arrays within a describe block should live at the same scope — either all inside or all outside. Don't mix.
common.tsReusable destination, connection, and override fixtures for component tests under test/integrations/destinations/<destination>/ go in common.ts and are imported by router/data.ts, processor/data.ts, etc. Don't define them inline at the top of data.ts. This includes per-scenario variants (e.g., a destination override that changes a single action's requestBody, or a connection variant with isHashRequired: true).
// Good — variants exported from common.ts alongside the base fixtures
// common.ts
export const destination = { /* ... */ };
export const connection = { /* ... */ };
export const hashRequiredConnection = {
...connection,
config: { destination: { ...connection.config.destination, isHashRequired: true } },
};
// router/data.ts
import { destination, connection, hashRequiredConnection } from '../common';
export const data = [
{ /* ... */, connection: hashRequiredConnection },
];
// Bad — variants declared at the top of router/data.ts
const hashRequiredConnection = { /* ... */ }; // belongs in common.ts
Each entry in the exported data array of test/integrations/destinations/<destination>/{router,processor}/data.ts is a plain object literal. Don't wrap an entry in an IIFE just to lift local consts — hoist those to top-level constants in common.ts (or near the top of the file if truly local) instead.
// Good — plain object entries; overrides hoisted to common.ts or top of file
import { customMappingsDestination, customMappingsConnection } from '../common';
export const data = [
{
id: 'test-4',
/* ... */,
input: { /* uses customMappingsDestination, customMappingsConnection */ },
},
];
// Bad — IIFE inside the array breaks the file's pattern
export const data = [
(() => {
const customMappingsDestination = { /* ... */ };
const customMappingsConnection = { /* ... */ };
return { id: 'test-4', /* ... */ };
})(),
];
When asserting on hashed identifiers in audience-destination tests, invoke the hash function in the test data (sha256('a@b.com')) instead of pasting a copied hex string. Generated values stay in sync with input edits and reviewers can scan inputs without decoding hex.
// Good
import sha256 from 'sha256';
users: [
{ email: sha256('a@b.com') },
{ email: sha256('c@d.com') },
],
// Bad — copied hex; rotting risk if the input email is ever changed
users: [
{ email: 'fb98d44ad7501a959f3f4f4a3f004fe2d9e581ea6207e218c4b02c08a4d75adf' },
{ email: '6a4d2afe9d4d6f5cb73d8e3e3a8fa8e5dc1d2a1f64d1b9e0c5b9b1c2d6e3f5a4' },
],
When testing a method that returns a structured response, assert the entire object with toEqual rather than picking individual fields. This catches unexpected extra fields and makes the expected shape explicit at a glance.
// Good — full response shape visible in one assertion
expect(result).toEqual({
users: [{ id: 'u1', status: 'active' }],
errors: [],
metadata: { total: 1 },
});
// Bad — individual field assertions hide the overall shape
expect(result.users).toEqual([{ id: 'u1', status: 'active' }]);
expect(result.errors).toEqual([]);
expect(result.metadata).toEqual({ total: 1 });
Prefer using the real module and mocking only its leaf dependencies (logger, stats) rather than replacing the whole module with a stub. Only mock the module itself if using it makes the test significantly more complex.
// Good — real DisposableCache, only leaf deps mocked
jest.mock('../../../../logger', () => ({
info: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}));
jest.mock('../../../../util/stats', () => ({
increment: jest.fn(),
gauge: jest.fn(),
}));
import { IvmScriptRunner } from './ivmScriptRunner'; // uses real DisposableCache
// Bad — entire intermediate module replaced with a stub
jest.mock('../../../../util/ivmCache/index', () => {
return class MockCache {
private store = new Map();
get(key: string) {
return this.store.get(key);
}
set(key: string, value: any) {
this.store.set(key, value);
}
delete(key: string) {
this.store.delete(key);
}
};
});