Agent skill
create-unit-test
Create Vitest unit tests following project patterns. Use when writing tests for functions, components, models, or route loaders/actions.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/create-unit-test
SKILL.md
Create Unit Test
Creates Vitest unit tests following Iridium testing patterns with proper mocking.
When to Use
- Writing tests for any code
- User asks to "add tests", "write unit tests", or "test this function"
- After implementing new features that need test coverage
Test File Location & Naming
Tests are co-located with source files:
app/
├── components/
│ ├── Button.tsx
│ └── Button.test.tsx # Component test
├── lib/
│ ├── form-validation.server.ts
│ └── form-validation.server.test.ts # Utility test
└── models/
├── user.server.ts
└── user.server.test.ts # Model test
Naming:
*.test.ts- TypeScript utility/library tests*.test.tsx- React component tests
Test Template
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('Feature Name', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('does something specific', () => {
// Arrange
const input = 'test';
// Act
const result = functionUnderTest(input);
// Assert
expect(result).toBe('expected');
});
});
Pattern 1: Component Testing
Use React Testing Library. Focus on user behavior, not implementation.
tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '~/test/utils';
import { Button } from './Button';
describe('Button Component', () => {
it('renders with default props', () => {
render(<Button>Click me</Button>);
const button = screen.getByRole('button', { name: /click me/i });
expect(button).toBeInTheDocument();
expect(button).toHaveClass('btn');
});
it('handles click events', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
screen.getByRole('button').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Key principles:
- Query by role, label, or text (NOT class or test IDs)
- Use semantic queries:
getByRole,getByLabelText,getByText - Test user interactions (clicks, typing)
Pattern 2: Utility/Library Testing
Test pure functions with input/output assertions.
typescript
import { describe, it, expect } from 'vitest';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { validateFormData } from './form-validation.server';
const testSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
});
describe('validateFormData', () => {
it('validates correct form data', async () => {
const formData = new FormData();
formData.append('name', 'John Doe');
formData.append('email', 'john@example.com');
const result = await validateFormData(
formData,
zodResolver(testSchema),
);
expect(result.data).toEqual({
name: 'John Doe',
email: 'john@example.com',
});
expect(result.errors).toBeUndefined();
});
it('returns errors for invalid data', async () => {
const formData = new FormData();
formData.append('name', '');
formData.append('email', 'invalid');
const result = await validateFormData(
formData,
zodResolver(testSchema),
);
expect(result.errors?.name).toBeDefined();
expect(result.errors?.email).toBeDefined();
});
});
Pattern 3: Model Layer Testing
Mock Prisma client. Verify correct queries and data transformations.
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Role } from '~/generated/prisma/client';
import { getUserProfile, updateUser } from './user.server';
// Mock BEFORE importing prisma
vi.mock('~/db.server', () => ({
prisma: {
user: {
findUnique: vi.fn(),
update: vi.fn(),
},
},
}));
import { prisma } from '~/db.server';
describe('User Model', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('fetches user profile by ID', async () => {
const mockProfile = {
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
role: Role.USER,
};
vi.mocked(prisma.user.findUnique).mockResolvedValue(mockProfile as any);
const result = await getUserProfile('user-123');
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: 'user-123' },
select: expect.objectContaining({
id: true,
email: true,
name: true,
}),
});
expect(result).toEqual(mockProfile);
});
});
Critical: Mock ~/db.server BEFORE importing it.
Pattern 4: Route Loader/Action Testing
Mock dependencies and verify responses.
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { loader, action } from './profile';
import { createMockRequest } from '~/test/utils';
// Mock dependencies
vi.mock('~/lib/session.server');
vi.mock('~/models/user.server');
import { requireUser } from '~/lib/session.server';
import { getUserProfile, updateUser } from '~/models/user.server';
describe('Profile Route', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('loader', () => {
it('returns user profile', async () => {
const mockUser = { id: 'user-123' };
const mockProfile = { id: 'user-123', name: 'Test' };
vi.mocked(requireUser).mockResolvedValue(mockUser as any);
vi.mocked(getUserProfile).mockResolvedValue(mockProfile as any);
const request = createMockRequest({
url: 'http://localhost:5173/profile',
});
const response = await loader({
request,
params: {},
context: {},
});
const data = await response.json();
expect(data.profile).toEqual(mockProfile);
});
});
describe('action', () => {
it('updates profile on PUT', async () => {
const mockUser = { id: 'user-123' };
vi.mocked(requireUser).mockResolvedValue(mockUser as any);
vi.mocked(updateUser).mockResolvedValue({ id: 'user-123' } as any);
const request = createMockRequest({
method: 'PUT',
url: 'http://localhost:5173/profile',
body: { name: 'New Name' },
});
const response = await action({
request,
params: {},
context: {},
});
const data = await response.json();
expect(data.success).toBe(true);
});
});
});
Test Utilities
Located in app/test/utils.tsx:
| Utility | Purpose |
|---|---|
renderWithProviders(ui) |
Render with React Router context |
createMockUser(overrides?) |
Create mock user object |
createMockRequest(options) |
Create mock Request for loaders/actions |
createMockFormData(data) |
Create FormData from object |
Mocking Patterns
Module Mocking
typescript
// Mock BEFORE importing
vi.mock('~/db.server', () => ({
prisma: {
user: { findUnique: vi.fn(), update: vi.fn() },
},
}));
import { prisma } from '~/db.server';
Async Module Mocking (with type safety)
typescript
// Type the importOriginal result
vi.mock('@react-email/components', async (importOriginal) => {
const actual = await importOriginal<typeof import('@react-email/components')>();
return {
...actual,
render: vi.fn().mockResolvedValue('<html>Mocked</html>'),
};
});
Function Mocking
typescript
const mockFn = vi.fn();
mockFn.mockReturnValue('value');
mockFn.mockResolvedValue({ data: 'async' });
mockFn.mockImplementation((arg) => `custom: ${arg}`);
Suppressing Console Output
typescript
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'error').mockImplementation(() => {});
});
What to Test
- Business rules, validation, permissions
- Data transformations, API call payloads
- User interactions (clicks, form submissions)
- Side effects (via mocks/spies)
- Error handling paths
- Loading, empty, success, error states
What NOT to Test
- Third-party libraries (React Router, DaisyUI)
- Pure styling/CSS classes
- Implementation details (private functions)
- Multiple variations of identical behavior
Running Tests
bash
npm test # Watch mode
npm run test:run # Single run (CI)
npm run test:ui # Visual UI
npm run test:coverage # Coverage report
Anti-Patterns
- Importing from
@prisma/client(use~/generated/prisma/client) - Testing implementation details instead of behavior
- Querying by CSS class or test IDs
- Not clearing mocks between tests
- Missing mock declarations before imports
Full Reference
See .github/instructions/unit-testing.instructions.md for comprehensive documentation.
Didn't find tool you were looking for?