Create or update test files for TypeScript classes that implement interfaces. Use when user asks to "create tests", "update tests", "generate test file", "fix tests", "test MyClass", or mentions needing tests for a class. Generates vitest test files with describe blocks, createClass helpers, and proper fake dependency injection.
This skill helps you create or update test files for classes that implement interfaces. Tests follow a specific pattern with vitest, fake builders for dependencies, and structured describe blocks.
Use this skill when you need to:
The skill will generate/update:
createMyClass helper function for dependency injectionInvoke this skill when the user asks to:
createMyClass helper to handle dependency creation and overridesmockReturnValue when possibleit block should test one specific scenario with ideally one assertionvi.mock for 3rd party dependencies, use Fakes for internal dependenciesThis skill supports testing two types of classes:
Entity classes that implement interfaces with event brokers (IEntity pattern):
name: IPropEventBroker<string>)Service or client classes that wrap dependencies (e.g., HTTP clients, APIs):
Key Difference for Service/Client Classes:
Before creating/updating tests:
.spec.ts fileIf test file exists: Update mode
If test file does NOT exist: Create mode
CRITICAL: Test files MUST be in the __tests__ folder, which is a SIBLING of the /src folder, NOT inside it.
packages/
my-package/
src/
MyClass.ts
IMyClass.ts
__tests__/ # Sibling to src/, NOT inside src/
myClass.spec.ts # camelCase filename
otherClass.spec.ts
For a class named MyClass:
myClass.spec.ts (camelCase, matches class name but lowercase first letter)packages/my-package/__tests__/myClass.spec.tsimport { vi, describe, it, expect, beforeEach } from 'vitest';
import type { IMyClass } from '../src/IMyClass';
import { MyClass } from '../src/MyClass';
import { FakeOtherClassBuilder } from '@someScope/some-library/fakes';
import type { IOtherClass } from '@someScope/some-library';
Import Rules:
'vitest'type import/fakes subpathdescribe('MyClass', () => {
// Helper function
function createMyClass(/* ... */): { /* ... */ } {
// ...
}
// Method describe blocks
describe('someMethod', () => {
// Test cases
});
describe('anotherMethod', () => {
// Test cases
});
});
Structure Rules:
createMyClass helper function at the topPurpose: Factory function that creates an instance of the class with all its dependencies, allowing for easy overrides in tests.
Standard Pattern:
function createMyClass(overrides?: {
otherClass?: IOtherClass;
anotherService?: IAnotherService;
}) {
const otherClass = overrides?.otherClass ?? new FakeOtherClassBuilder().build();
const anotherService = overrides?.anotherService ?? new FakeAnotherServiceBuilder().build();
const myClass = new MyClass({
otherClass,
anotherService
});
return {
otherClass,
anotherService,
myClass
};
}
Rules:
overrides parameter (optional object)??) to provide default Fake instancesAdvanced Pattern (with primitive parameters):
function createMyClass(
params?: {
initialValue?: string;
config?: { timeout: number };
},
overrides?: {
otherClass?: IOtherClass;
}
) {
const otherClass = overrides?.otherClass ?? new FakeOtherClassBuilder().build();
const myClass = new MyClass({
initialValue: params?.initialValue ?? 'default',
config: params?.config ?? { timeout: 1000 },
otherClass
});
return {
otherClass,
myClass
};
}
One describe per interface method:
describe('login', () => {
it('should return true when credentials are valid', () => {
const { myClass, authService } = createMyClass({
authService: new FakeAuthServiceBuilder()
.withValidateReturnValue(Promise.resolve(true))
.build()
});
const result = await myClass.login('user', 'pass');
expect(result).toBe(true);
});
it('should return false when credentials are invalid', () => {
const { myClass, authService } = createMyClass({
authService: new FakeAuthServiceBuilder()
.withValidateReturnValue(Promise.resolve(false))
.build()
});
const result = await myClass.login('user', 'wrong');
expect(result).toBe(false);
});
it('should call authService.validate with correct parameters', () => {
const { myClass, authService } = createMyClass();
await myClass.login('testuser', 'testpass');
expect(authService.validate).toHaveBeenCalledWith('testuser', 'testpass');
});
});
Rules for Test Cases:
it block per test scenarioPreferred Method (Fake Builders):
it('should handle success case', () => {
const { myClass } = createMyClass({
otherService: new FakeOtherServiceBuilder()
.withProcessReturnValue(Promise.resolve({ success: true }))
.withIsActiveValue(true)
.build()
});
// Test using the configured fake
});
Avoid (mockReturnValue) unless absolutely necessary:
// DON'T DO THIS unless impossible to avoid
it('should handle changing return values', () => {
const { myClass, otherService } = createMyClass();
// ONLY use this as a LAST RESORT
otherService.process.mockReturnValue(Promise.resolve({ success: false }));
// Test...
});
When mockReturnValue is Acceptable:
Good Pattern:
it('should call dependency method with correct arguments', () => {
const { myClass, otherService } = createMyClass();
myClass.processData({ id: '123', name: 'test' });
expect(otherService.process).toHaveBeenCalledWith({ id: '123', name: 'test' });
expect(otherService.process).toHaveBeenCalledTimes(1);
});
Testing with Multiple Calls:
it('should call dependency multiple times', () => {
const { myClass, otherService } = createMyClass();
myClass.processBatch([item1, item2, item3]);
expect(otherService.process).toHaveBeenCalledTimes(3);
expect(otherService.process).toHaveBeenNthCalledWith(1, item1);
expect(otherService.process).toHaveBeenNthCalledWith(2, item2);
expect(otherService.process).toHaveBeenNthCalledWith(3, item3);
});
When to use:
Example:
import { vi, describe, it, expect } from 'vitest';
import axios from 'axios';
vi.mock('axios');
describe('ApiClient', () => {
it('should fetch data from API', async () => {
vi.mocked(axios.get).mockResolvedValue({ data: { id: 1 } });
const client = new ApiClient();
const result = await client.fetchUser(1);
expect(result).toEqual({ id: 1 });
expect(axios.get).toHaveBeenCalledWith('/users/1');
});
});
/fakes)__tests__ folder exists - Check if __tests__/ directory exists at package root (sibling to /src), create if needed__tests__/myClass.spec.ts with:
createMyClass helper functionit blocks per method covering different scenarioscreateMyClass function and importscreateMyClass and importscreateMyClass functionWhen updating an existing test:
describe('fetchData', () => {
it('should return data when fetch succeeds', async () => {
const expectedData = { id: 1, name: 'Test' };
const { myClass } = createMyClass({
apiClient: new FakeApiClientBuilder()
.withGetReturnValue(Promise.resolve(expectedData))
.build()
});
const result = await myClass.fetchData('123');
expect(result).toEqual(expectedData);
});
it('should throw error when fetch fails', async () => {
const { myClass } = createMyClass({
apiClient: new FakeApiClientBuilder()
.withGetReturnValue(Promise.reject(new Error('Network error')))
.build()
});
await expect(myClass.fetchData('123')).rejects.toThrow('Network error');
});
});
describe('updateUser', () => {
it('should call repository with user ID and updates', async () => {
const { myClass, userRepository } = createMyClass();
await myClass.updateUser('user-123', { name: 'New Name', email: 'new@email.com' });
expect(userRepository.update).toHaveBeenCalledWith('user-123', {
name: 'New Name',
email: 'new@email.com'
});
});
it('should handle empty updates object', async () => {
const { myClass, userRepository } = createMyClass();
await myClass.updateUser('user-123', {});
expect(userRepository.update).toHaveBeenCalledWith('user-123', {});
});
it('should handle undefined user ID', async () => {
const { myClass } = createMyClass();
await expect(myClass.updateUser(undefined, { name: 'Test' }))
.rejects.toThrow('User ID is required');
});
});
describe('processOrder', () => {
it('should use express shipping when order is marked as urgent', async () => {
const { myClass, shippingService } = createMyClass();
await myClass.processOrder({ id: '123', urgent: true });
expect(shippingService.ship).toHaveBeenCalledWith(
expect.objectContaining({ shippingMethod: 'express' })
);
});
it('should use standard shipping when order is not urgent', async () => {
const { myClass, shippingService } = createMyClass();
await myClass.processOrder({ id: '123', urgent: false });
expect(shippingService.ship).toHaveBeenCalledWith(
expect.objectContaining({ shippingMethod: 'standard' })
);
});
it('should apply discount when customer is premium', async () => {
const { myClass } = createMyClass({
customerService: new FakeCustomerServiceBuilder()
.withIsPremiumReturnValue(true)
.build()
});
const result = await myClass.processOrder({ id: '123', total: 100 });
expect(result.total).toBe(90); // 10% discount
});
it('should not apply discount for regular customers', async () => {
const { myClass } = createMyClass({
customerService: new FakeCustomerServiceBuilder()
.withIsPremiumReturnValue(false)
.build()
});
const result = await myClass.processOrder({ id: '123', total: 100 });
expect(result.total).toBe(100);
});
});
describe('UserManager', () => {
function createUserManager(/* ... */) {
// ...
}
describe('addUser', () => {
let userManager: IUserManager;
let userRepository: IUserRepository;
beforeEach(() => {
const created = createUserManager();
userManager = created.userManager;
userRepository = created.userRepository;
});
it('should add user to repository', () => {
userManager.addUser({ id: '1', name: 'John' });
expect(userRepository.save).toHaveBeenCalledWith({ id: '1', name: 'John' });
});
it('should validate user before adding', () => {
userManager.addUser({ id: '1', name: 'John' });
expect(userRepository.validate).toHaveBeenCalled();
});
});
});
Note: Use beforeEach sparingly. It's useful when many tests need the same setup, but can make tests harder to understand. Prefer explicit setup in each test when possible.
describe('updateName', () => {
it('should update the name event broker', () => {
const { myClass } = createMyClass();
myClass.updateName('New Name');
expect(myClass.name.get()).toBe('New Name');
});
it('should notify listeners when name changes', () => {
const { myClass } = createMyClass();
const listener = vi.fn();
myClass.name.subscribe(listener);
myClass.updateName('New Name');
expect(listener).toHaveBeenCalledWith('New Name');
});
});
For service/client classes that wrap HTTP clients or APIs:
describe('ReportsClient', () => {
function createReportsClient(overrides?: {
httpClient?: IHttpClient;
}) {
const httpClient = overrides?.httpClient ?? new FakeHttpClientBuilder()
.withGetCallback(() => Promise.resolve({ data: [], status: 200, headers: {} }))
.build();
const reportsClient = new ReportsClient(httpClient);
return {
httpClient,
reportsClient
};
}
describe('listReportTemplates', () => {
it('should call httpClient.get with correct endpoint', async () => {
const { reportsClient, httpClient } = createReportsClient();
await reportsClient.listReportTemplates();
expect(httpClient.get).toHaveBeenCalledWith('/velocity/reports');
});
it('should transform backend DTOs to frontend DTOs', async () => {
/* eslint-disable camelcase */
const backendDTO: ReportBackendDTO = {
id: 'report-1',
name: 'Test ReportTemplate',
report_type: 'analytics',
description: 'Test Description',
notebook_path: '/path/to/notebook',
parameters: { key: 'value' },
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-02T00:00:00Z'
};
/* eslint-enable camelcase */
const { reportsClient } = createReportsClient({
httpClient: new FakeHttpClientBuilder()
.withGetCallback(() => Promise.resolve({ data: [backendDTO], status: 200, headers: {} }))
.build()
});
const result = await reportsClient.listReportTemplates();
expect(result.reports).toHaveLength(1);
expect(result.reports[0]).toEqual({
id: 'report-1',
name: 'Test ReportTemplate',
reportType: 'analytics',
description: 'Test Description',
notebookPath: '/path/to/notebook',
parameters: { key: 'value' },
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z'
});
});
it('should return empty array when no reports are available', async () => {
const { reportsClient } = createReportsClient({
httpClient: new FakeHttpClientBuilder()
.withGetCallback(() => Promise.resolve({ data: [], status: 200, headers: {} }))
.build()
});
const result = await reportsClient.listReportTemplates();
expect(result.reports).toEqual([]);
});
});
describe('createReportTemplate', () => {
it('should call httpClient.post with transformed data', async () => {
/* eslint-disable camelcase */
const backendDTO: ReportBackendDTO = {
id: 'report-1',
name: 'New ReportTemplate',
report_type: 'test-type',
description: 'Test Description',
notebook_path: '/notebook',
parameters: { param: 'value' },
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z'
};
/* eslint-enable camelcase */
const { reportsClient, httpClient } = createReportsClient({
httpClient: new FakeHttpClientBuilder()
.withPostCallback(() => Promise.resolve({ data: backendDTO, status: 201, headers: {} }))
.build()
});
await reportsClient.createReportTemplate({
name: 'New ReportTemplate',
reportType: 'test-type',
description: 'Test Description',
notebookPath: '/notebook',
parameters: { param: 'value' }
});
expect(httpClient.post).toHaveBeenCalledWith('/velocity/reports', {
name: 'New ReportTemplate',
report_type: 'test-type',
description: 'Test Description',
notebook_path: '/notebook',
parameters: { param: 'value' }
});
});
it('should return created report in response object', async () => {
/* eslint-disable camelcase */
const backendDTO: ReportBackendDTO = {
id: 'report-1',
name: 'New ReportTemplate',
report_type: 'test-type',
description: null,
notebook_path: null,
parameters: null,
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z'
};
/* eslint-enable camelcase */
const { reportsClient } = createReportsClient({
httpClient: new FakeHttpClientBuilder()
.withPostCallback(() => Promise.resolve({ data: backendDTO, status: 201, headers: {} }))
.build()
});
const result = await reportsClient.createReportTemplate({
name: 'New ReportTemplate',
reportType: 'test-type'
});
expect(result.report.id).toBe('report-1');
expect(result.report.name).toBe('New ReportTemplate');
expect(result.report.reportType).toBe('test-type');
});
});
});
Key Points for Service/Client Tests:
// Good
it('should return null when user is not found', () => { /* ... */ });
it('should throw error when email is invalid', () => { /* ... */ });
// Bad
it('should work', () => { /* ... */ });
it('test login', () => { /* ... */ });
it('should calculate total with tax', () => {
// Arrange
const { calculator } = createCalculator();
// Act
const result = calculator.calculateTotal(100, 0.1);
// Assert
expect(result).toBe(110);
})
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add talbenmoshe/manage-entity-tests下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer