growilabs / essential-test-patterns
Install for your project team
Run this command in your project directory to install the skill for your entire team:
mkdir -p .claude/skills/essential-test-patterns && curl -L -o skill.zip "https://fastmcp.me/Skills/Download/4091" && unzip -o skill.zip -d .claude/skills/essential-test-patterns && rm skill.zip
Project Skills
This skill will be saved in .claude/skills/essential-test-patterns/ and checked into git. All team members will have access to it automatically.
Important: Please verify the skill by reviewing its instructions before using it.
GROWI testing patterns with Vitest, React Testing Library, and vitest-mock-extended.
0 views
0 installs
Skill Content
---
name: essential-test-patterns
description: GROWI testing patterns with Vitest, React Testing Library, and vitest-mock-extended.
---
# GROWI Testing Patterns
GROWI uses **Vitest** for all testing (unit, integration, component). This skill covers universal testing patterns applicable across the monorepo.
## Test File Placement (Global Standard)
Place test files **in the same directory** as the source file:
```
src/components/Button/
├── Button.tsx
└── Button.spec.tsx # Component test
src/utils/
├── helper.ts
└── helper.spec.ts # Unit test
src/services/api/
├── pageService.ts
└── pageService.integ.ts # Integration test
```
## Test Types & Environments
| File Pattern | Type | Environment | Use Case |
|--------------|------|-------------|----------|
| `*.spec.{ts,js}` | Unit Test | Node.js | Pure functions, utilities, services |
| `*.integ.ts` | Integration Test | Node.js + DB | API routes, database operations |
| `*.spec.{tsx,jsx}` | Component Test | happy-dom | React components |
Vitest automatically selects the environment based on file extension and configuration.
## Vitest Configuration
### Global APIs (No Imports Needed)
All GROWI packages configure Vitest globals in `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
```
This enables auto-import of testing APIs:
```typescript
// No imports needed!
describe('MyComponent', () => {
it('should render', () => {
expect(true).toBe(true);
});
beforeEach(() => {
// Setup
});
afterEach(() => {
// Cleanup
});
});
```
**Available globals**: `describe`, `it`, `test`, `expect`, `beforeEach`, `afterEach`, `beforeAll`, `afterAll`, `vi`
## Type-Safe Mocking with vitest-mock-extended
### Basic Usage
`vitest-mock-extended` provides **fully type-safe mocks** with TypeScript autocomplete:
```typescript
import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended';
// Create type-safe mock
const mockRouter: DeepMockProxy<NextRouter> = mockDeep<NextRouter>();
// TypeScript autocomplete works!
mockRouter.asPath = '/test-path';
mockRouter.query = { id: '123' };
mockRouter.push.mockResolvedValue(true);
// Use in tests
expect(mockRouter.push).toHaveBeenCalledWith('/new-path');
```
### Complex Types with Optional Properties
```typescript
interface ComplexProps {
currentPageId?: string | null;
currentPathname?: string | null;
data?: Record<string, unknown>;
onSubmit?: (value: string) => void;
}
const mockProps: DeepMockProxy<ComplexProps> = mockDeep<ComplexProps>();
mockProps.currentPageId = 'page-123';
mockProps.data = { key: 'value' };
mockProps.onSubmit?.mockImplementation((value) => {
console.log(value);
});
```
### Why vitest-mock-extended?
- ✅ **Type safety**: Catches typos at compile time
- ✅ **Autocomplete**: IDE suggestions for all properties/methods
- ✅ **Deep mocking**: Automatically mocks nested objects
- ✅ **Vitest integration**: Works seamlessly with `vi.fn()`
## React Testing Library Patterns
### Basic Component Test
```typescript
import { render } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('should render with text', () => {
const { getByText } = render(<Button>Click me</Button>);
expect(getByText('Click me')).toBeInTheDocument();
});
it('should call onClick when clicked', async () => {
const onClick = vi.fn();
const { getByRole } = render(<Button onClick={onClick}>Click</Button>);
const button = getByRole('button');
await userEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});
});
```
### Testing with Jotai (Global Pattern)
When testing components that use Jotai atoms, wrap with `<Provider>`:
```typescript
import { render } from '@testing-library/react';
import { Provider } from 'jotai';
const renderWithJotai = (ui: React.ReactElement) => {
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<Provider>{children}</Provider>
);
return render(ui, { wrapper: Wrapper });
};
describe('ComponentWithJotai', () => {
it('should render with atom state', () => {
const { getByText } = renderWithJotai(<MyComponent />);
expect(getByText('Hello')).toBeInTheDocument();
});
});
```
### Isolated Jotai Scope (For Testing)
To isolate atom state between tests:
```typescript
import { createScope } from 'jotai-scope';
describe('ComponentWithIsolatedState', () => {
it('test 1', () => {
const scope = createScope();
const { getByText } = renderWithJotai(<MyComponent />, scope);
// ...
});
it('test 2', () => {
const scope = createScope(); // Fresh scope
const { getByText } = renderWithJotai(<MyComponent />, scope);
// ...
});
});
```
## Async Testing Patterns (Global Standard)
### Using `act()` and `waitFor()`
When testing async state updates:
```typescript
import { waitFor, act } from '@testing-library/react';
import { renderHook } from '@testing-library/react';
test('async hook', async () => {
const { result } = renderHook(() => useMyAsyncHook());
// Trigger async action
await act(async () => {
result.current.triggerAsyncAction();
});
// Wait for state update
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeDefined();
});
```
### Testing Async Functions
```typescript
it('should fetch data successfully', async () => {
const data = await fetchData();
expect(data).toEqual({ id: '123', name: 'Test' });
});
it('should handle errors', async () => {
await expect(fetchDataWithError()).rejects.toThrow('Error');
});
```
## Advanced Assertions
### Object Matching
```typescript
expect(mockFunction).toHaveBeenCalledWith(
expect.objectContaining({
pathname: '/expected-path',
data: expect.any(Object),
timestamp: expect.any(Number),
})
);
```
### Array Matching
```typescript
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: '123' }),
expect.objectContaining({ id: '456' }),
])
);
```
### Partial Matching
```typescript
expect(user).toMatchObject({
name: 'John',
email: 'john@example.com',
// Other properties are ignored
});
```
## Test Structure Best Practices
### AAA Pattern (Arrange-Act-Assert)
```typescript
describe('MyComponent', () => {
beforeEach(() => {
vi.clearAllMocks(); // Clear mocks before each test
});
describe('rendering', () => {
it('should render with default props', () => {
// Arrange: Setup test data
const props = { title: 'Test' };
// Act: Render component
const { getByText } = render(<MyComponent {...props} />);
// Assert: Verify output
expect(getByText('Test')).toBeInTheDocument();
});
});
describe('user interactions', () => {
it('should submit form on button click', async () => {
// Arrange
const onSubmit = vi.fn();
const { getByRole, getByLabelText } = render(
<MyForm onSubmit={onSubmit} />
);
// Act
await userEvent.type(getByLabelText('Name'), 'John');
await userEvent.click(getByRole('button', { name: 'Submit' }));
// Assert
expect(onSubmit).toHaveBeenCalledWith({ name: 'John' });
});
});
});
```
### Nested `describe` for Organization
```typescript
describe('PageService', () => {
describe('createPage', () => {
it('should create a page successfully', async () => {
// ...
});
it('should throw error if path is invalid', async () => {
// ...
});
});
describe('updatePage', () => {
it('should update page content', async () => {
// ...
});
});
});
```
## Common Mocking Patterns
### Mocking SWR
```typescript
vi.mock('swr', () => ({
default: vi.fn(() => ({
data: mockData,
error: null,
isLoading: false,
mutate: vi.fn(),
})),
}));
```
### Mocking Modules
```typescript
// Mock entire module
vi.mock('~/services/PageService', () => ({
PageService: {
findById: vi.fn().mockResolvedValue({ id: '123', title: 'Test' }),
create: vi.fn().mockResolvedValue({ id: '456', title: 'New' }),
},
}));
// Use in test
import { PageService } from '~/services/PageService';
it('should call PageService.findById', async () => {
await myFunction();
expect(PageService.findById).toHaveBeenCalledWith('123');
});
```
### Mocking Specific Functions
```typescript
import { myFunction } from '~/utils/myUtils';
vi.mock('~/utils/myUtils', () => ({
myFunction: vi.fn().mockReturnValue('mocked'),
otherFunction: vi.fn(), // Mock other exports
}));
```
### Mocking CommonJS Modules with mock-require
**IMPORTANT**: When `vi.mock()` fails with ESModule/CommonJS compatibility issues, use `mock-require` instead:
```typescript
import mockRequire from 'mock-require';
describe('Service with CommonJS dependencies', () => {
beforeEach(() => {
// Mock CommonJS module before importing the code under test
mockRequire('legacy-module', {
someFunction: vi.fn().mockReturnValue('mocked'),
someProperty: 'mocked-value',
});
});
afterEach(() => {
// Clean up mocks to avoid leakage between tests
mockRequire.stopAll();
});
it('should use mocked module', async () => {
// Import AFTER mocking (dynamic import if needed)
const { MyService } = await import('~/services/MyService');
const result = MyService.doSomething();
expect(result).toBe('mocked');
});
});
```
**When to use `mock-require`**:
- Legacy CommonJS modules that don't work with `vi.mock()`
- Mixed ESM/CJS environments causing module resolution issues
- Third-party libraries with complex module systems
- When `vi.mock()` fails with "Cannot redefine property" or "Module is not defined"
**Key points**:
- ✅ Mock **before** importing the code under test
- ✅ Use `mockRequire.stopAll()` in `afterEach()` to prevent test leakage
- ✅ Use dynamic imports (`await import()`) when needed
- ✅ Works with both CommonJS and ESModule targets
### Choosing the Right Mocking Strategy
```typescript
// ✅ Prefer vi.mock() for ESModules (simplest)
vi.mock('~/modern-module', () => ({
myFunction: vi.fn(),
}));
// ✅ Use mock-require for CommonJS or mixed environments
import mockRequire from 'mock-require';
mockRequire('legacy-module', { myFunction: vi.fn() });
// ✅ Use vitest-mock-extended for type-safe object mocks
import { mockDeep } from 'vitest-mock-extended';
const mockService = mockDeep<MyService>();
```
**Decision tree**:
1. Can use `vi.mock()`? → Use it (simplest)
2. CommonJS or module error? → Use `mock-require`
3. Need type-safe object mock? → Use `vitest-mock-extended`
## Integration Tests (with Database)
Integration tests (*.integ.ts) can access in-memory databases:
```typescript
describe('PageService Integration', () => {
beforeEach(async () => {
// Setup: Seed test data
await Page.create({ path: '/test', body: 'content' });
});
afterEach(async () => {
// Cleanup: Clear database
await Page.deleteMany({});
});
it('should create a page', async () => {
const page = await PageService.create({
path: '/new-page',
body: 'content',
});
expect(page._id).toBeDefined();
expect(page.path).toBe('/new-page');
});
});
```
## Testing Checklist
Before committing tests, ensure:
- ✅ **Co-location**: Test files are next to source files
- ✅ **Descriptive names**: Test descriptions clearly state what is being tested
- ✅ **AAA pattern**: Tests follow Arrange-Act-Assert structure
- ✅ **Mocks cleared**: Use `beforeEach(() => vi.clearAllMocks())`
- ✅ **Async handled**: Use `async/await` and `waitFor()` for async operations
- ✅ **Type safety**: Use `vitest-mock-extended` for type-safe mocks
- ✅ **Isolated state**: Jotai tests use separate scopes if needed
## Running Tests
See the `testing` rule (`.claude/rules/testing.md`) for test execution commands.
## Summary: GROWI Testing Philosophy
1. **Co-locate tests**: Keep tests close to source code
2. **Type-safe mocks**: Use `vitest-mock-extended` for TypeScript support
3. **React Testing Library**: Test user behavior, not implementation details
4. **Async patterns**: Use `act()` and `waitFor()` for async state updates
5. **Jotai integration**: Wrap components with `<Provider>` for atom state
6. **Clear structure**: Use nested `describe` and AAA pattern
7. **Clean mocks**: Always clear mocks between tests
These patterns apply to **all GROWI packages** with React/TypeScript code.