| name | typescript-testing |
| description | Mandatory testing conventions for TypeScript stacks (Vitest for CLI/SDK/Website/CDK). Use for unit, integration, or E2E tests with Vitest or Playwright. |
Testing Conventions (TypeScript)
This skill defines the MANDATORY testing conventions for TypeScript projects.
These are rules, not guidelines.
Documentation Rules
NO Comments Except AAA Markers
- Do NOT write explanatory comments — code must be self-explanatory
- Only
// Arrange, // Act, // Assert comments are allowed in tests
- The test name
Should_X_When_Y already documents the intent
Exception — SDK public API: Code under src/sdks/*/ consumed by
external developers (facade classes, public entry points) SHOULD have
JSDoc with usage examples. This exception does not apply to tests or
internal helpers.
Stacks Overview
| Stack | Test Runner | Location |
|---|
| CLI / Core | Vitest | tests/envilder/ |
| Node.js SDK | Vitest | tests/sdks/nodejs/ |
| Website | Vitest | tests/website/ |
| CDK (IaC) | Vitest | tests/iac/ |
| E2E | Vitest + TestContainers | e2e/ |
Core Principles
1. AAA Pattern (Arrange – Act – Assert)
ALL tests MUST follow the AAA pattern, separated by inline comments.
Rules
- Each phase MUST be separated with comments
- Never mix phases
- Each comment (
// Arrange, // Act, // Assert) appears AT MOST ONCE
per test — if you need two actions or two asserts, write two tests
- Act = one single invocation on the SUT. Multiple statements in Act only
if they are genuinely part of the same logical action (rare and exceptional).
Two independent operations = two tests.
- All assertions belong in Assert only. No
expect() in Arrange or Act.
If you feel tempted to assert in Arrange (precondition check), extract it to
a separate test or use a guard clause that throws — not an assertion.
- AAA markers are mandatory in ALL tests — including structural guards,
static completeness checks, and data validation tests. No exceptions.
- No
if, switch, or conditional logic inside Arrange, Act, or Assert
- No
try/catch/finally inside tests — use beforeEach/afterEach for
teardown
- No
// Act & Assert combined blocks — Act and Assert are ALWAYS separate
- For exceptions:
expect(() => action()).toThrow() or
await expect(asyncAction()).rejects.toThrow()
- Omit comment if section is empty
- If a test needs branching, split it into separate tests (one per scenario)
Vitest Example
it('Should_GenerateEnvFile_When_ValidParametersProvided', async () => {
mockStore.getMapping.mockResolvedValue({ KEY: '/ssm/path' });
mockProvider.getSecret.mockResolvedValue('value');
await sut.handle(command);
expect(mockStore.saveEnvironment).toHaveBeenCalled();
});
2. Test Naming Convention
Test names MUST follow exactly:
Should_{ExpectedBehavior}_When_{Condition}
Rules
- PascalCase for both parts
- NO natural language sentences in
it() descriptions
- NO vague names (
Should_Work, TestHandler)
- NO missing
When clause
Good Examples
| Test Name | Scenario |
|---|
Should_ThrowError_When_SSMParameterIsNotFound | Error path |
Should_ReturnMaskedValue_When_SecretIsLoaded | Success path |
Should_ReturnEmptyMap_When_NoMappingsExist | Edge case |
Variable Naming (MANDATORY)
| Purpose | Name |
|---|
| Subject under test | sut |
| Expected value | expected |
| Actual result | actual |
No creativity allowed here.
Mocking (Vitest)
Port Test Doubles
const mockProvider: ISecretProvider = {
getSecret: vi.fn(),
};
const mockStore: IVariableStore = {
getMapping: vi.fn(),
saveEnvironment: vi.fn(),
};
const mockLogger: ILogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
Controlling Behavior
vi.mocked(mockProvider.getSecret).mockResolvedValue('secret-value');
vi.mocked(mockProvider.getSecret).mockRejectedValue(new Error('not found'));
vi.mocked(mockStore.getMapping).mockResolvedValue({ KEY: '/path' });
Verifying Interactions
expect(mockProvider.getSecret).toHaveBeenCalledWith('/ssm/path');
expect(mockProvider.getSecret).toHaveBeenCalledTimes(1);
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('success'));
Exception Testing
Act and Assert MUST be separate. Wrap the action in Act, assert in Assert.
✅ CORRECT — separate Act and Assert
it('Should_ThrowInvalidArgument_When_ProviderIsUnsupported', () => {
const config = { provider: 'unsupported' };
const act = () => createProvider(config);
expect(act).toThrow(InvalidArgumentError);
});
✅ CORRECT — async exceptions
it('Should_ThrowParameterNotFound_When_SSMKeyIsMissing', async () => {
mockProvider.getSecret.mockRejectedValue(new ParameterNotFoundError('/missing'));
const act = sut.handle(command);
await expect(act).rejects.toThrow(ParameterNotFoundError);
});
❌ FORBIDDEN — combined Act & Assert
it('Should_ThrowError_When_Invalid', () => {
expect(() => sut.handle(bad)).toThrow();
});
Test Class Structure
CLI / Core (Vitest + InversifyJS handlers)
describe('PullSecretsToEnvCommandHandler', () => {
let sut: PullSecretsToEnvCommandHandler;
let mockStore: { getMapping: Mock; saveEnvironment: Mock };
let mockProvider: { getSecret: Mock };
let mockLogger: { info: Mock; warn: Mock; error: Mock };
beforeEach(() => {
mockStore = { getMapping: vi.fn(), saveEnvironment: vi.fn() };
mockProvider = { getSecret: vi.fn() };
mockLogger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
sut = new PullSecretsToEnvCommandHandler(
mockProvider,
mockStore,
mockLogger,
);
});
it('Should_GenerateEnvFile_When_ValidParametersProvided', async () => {
mockStore.getMapping.mockResolvedValue({ KEY: '/ssm/path' });
mockProvider.getSecret.mockResolvedValue('value');
await sut.handle(command);
expect(mockStore.saveEnvironment).toHaveBeenCalled();
});
});
Node.js SDK (Vitest, no DI)
describe('EnvilderClient', () => {
it('Should_ResolveSecrets_When_ProviderReturnsValues', async () => {
const mockProvider: ISecretProvider = {
getSecrets: vi.fn().mockResolvedValue(new Map([['KEY', 'value']])),
};
const sut = new EnvilderClient(mockProvider);
const actual = await sut.resolveSecrets(parsedMapFile);
expect(actual.get('KEY')).toBe('value');
});
});
CDK / IaC (Vitest)
describe('AppStack', () => {
it('Should_CreateLambda_When_StackSynthesized', () => {
const app = new cdk.App();
const stack = new AppStack(app, 'TestStack');
const actual = Template.fromStack(stack);
actual.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs22.x',
Handler: 'index.handler',
});
});
});
Structural Guards
Tests that validate static data (i18n completeness, schema conformance,
version consistency) still MUST use AAA markers. Arrange can be omitted
if data is set up at the describe level:
it('Should_HaveNoMissingKeys_When_CatalanComparedToEnglish', () => {
const actual = enKeys.filter((k) => !caKeys.includes(k));
expect(actual, 'Keys missing in ca.ts').toEqual([]);
});
Verification Commands
| Context | Command |
|---|
| CLI / Core / Website | pnpm test |
| Node.js SDK | cd tests/sdks/nodejs && pnpm vitest run --reporter=verbose |
| CDK | cd tests/iac && pnpm test |
| CI parity | pnpm test:ci |
| Lint | pnpm lint |
| Format | pnpm format |
Decision Points
- Pure domain logic → unit test at domain layer
- Handler orchestrating ports → mock ports with
vi.fn()
- AWS integration semantics → E2E with LocalStack/TestContainers
- Static data consistency → structural guard
- CDK infrastructure → snapshot + fine-grained assertions
Completion Criteria
- Test names follow
Should_<Expected>_When_<Condition>
- AAA markers present, each at most once
sut, actual, expected used consistently
- Positive and negative paths covered
- Tests run green:
pnpm test
- No lint regressions:
pnpm lint