Test unit
🦖Curated Cursor AI agent skills, slash commands, MCP configs, subagents & rules for full-stack dev — React 19, Next.js 15, Supabase, Tailwind v4, TypeScript
npx -y skills add kensaurus/cursor-kenji --skill test-unitAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write effective unit tests with best practices for any project. Auto-detects test framework (Jest, Vitest, pytest, Go test, etc.), researches current testing patterns via Firecrawl, fetches testing library docs via Context7, and uses Sentry MCP to identify production errors that lack test coverage. Use when writing tests, creating test cases, improving test coverage, increasing confidence before release, or when the user mentions testing.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
12.7 KB, as published. Nobody here has run it
Unit Testing Skill
Write effective, maintainable unit tests informed by the project's actual framework, production error data, and current best practices.
Step 0: Auto-Detect Test Environment
Before writing any tests, discover the project's testing setup.
0a. Detect Test Framework
Read the dependency manifest (package.json, requirements.txt, pyproject.toml, go.mod,
Cargo.toml, build.gradle, Gemfile) and look for:
| Framework | Detection Signal |
|---|---|
| Vitest | vitest in devDependencies, vitest.config.ts |
| Jest | jest in devDependencies, jest.config.*, "jest" key in package.json |
| Testing Library | @testing-library/react, @testing-library/vue, etc. |
| Playwright | @playwright/test in devDependencies, playwright.config.ts |
| Cypress | cypress in devDependencies, cypress.config.* |
| pytest | pytest in requirements, conftest.py files |
| Go test | _test.go files, go test in Makefile |
| RSpec | rspec in Gemfile, spec/ directory |
| JUnit | junit in build.gradle, src/test/ directory |
0b. Discover Existing Test Patterns
Glob: **/*.test.{ts,tsx,js,jsx} → JS/TS test files
Glob: **/*.spec.{ts,tsx,js,jsx} → JS/TS spec files
Glob: **/test_*.py → Python test files
Glob: **/*_test.go → Go test files
Glob: **/spec/**/*_spec.rb → Ruby spec files
Read 2-3 existing test files to understand:
- Import patterns and test utilities used
- Naming conventions (
describe/itvstest) - Mock/stub patterns (
vi.mock,jest.mock,unittest.mock) - Setup/teardown patterns (
beforeEach,afterEach, fixtures) - Assertion library (
expect,assert,chai)
0c. Detect Test Configuration
Glob: **/vitest.config.*
Glob: **/jest.config.*
Glob: **/pytest.ini
Glob: **/conftest.py
Glob: **/setup.cfg
Glob: **/.nycrc*
Glob: **/c8.config.*
Extract: coverage thresholds, test directories, global setup files, module aliases.
0d. Check Coverage Status
If there's a coverage tool configured (c8, istanbul/nyc, coverage.py):
# Check if coverage script exists
Grep: "coverage" in package.json scripts section
Grep: "c8" or "nyc" or "istanbul" in package.json
0e. Record Discovery
TEST ENVIRONMENT:
- Framework: [Vitest/Jest/pytest/etc. + version]
- Assertion style: [expect/assert/chai]
- Component testing: [Testing Library/Enzyme/none]
- Mock pattern: [vi.mock/jest.mock/unittest.mock]
- Coverage tool: [c8/nyc/coverage.py/none]
- Coverage threshold: [X% or not configured]
- Test directory: [__tests__/tests/spec/co-located]
- Existing tests: [count]
- Config file: [path]
Step 1: Research Testing Best Practices
Before writing tests, research current best practices for the detected framework.
1a. Context7 — Official Testing Library Docs
Fetch docs for the detected test framework:
context7:resolve-library-id
{
"libraryName": "<DETECTED_FRAMEWORK>",
"query": "unit testing best practices"
}
Then fetch specific guidance:
context7:query-docs
{
"libraryId": "<RESOLVED_ID>",
"query": "testing patterns mocking setup teardown"
}
Do this for each major testing dependency (e.g., vitest, @testing-library/react, msw).
1b. Firecrawl — Current Testing Patterns
firecrawl:firecrawl_search
{
"query": "<FRAMEWORK> unit testing best practices [current year]",
"limit": 5,
"sources": [{ "type": "web" }]
}
Run 2-3 searches targeting different aspects:
| Search | Query |
|---|---|
| Best practices | <framework> testing best practices [current year] |
| Mocking patterns | <framework> mocking API calls best practices |
| Component testing | React Testing Library component testing patterns [current year] |
| Coverage strategy | unit test coverage strategy meaningful tests vs coverage percentage |
Scrape the 1-2 most authoritative results for implementation details:
firecrawl:firecrawl_scrape
{
"url": "<AUTHORITATIVE_URL>",
"formats": ["markdown"],
"onlyMainContent": true
}
Step 2: Analyze Coverage Gaps (Sentry Integration)
Use Sentry MCP to find production errors that should have been caught by tests.
2a. Find Unresolved Production Errors
sentry:search_issues
{
"organizationSlug": "<ORG_SLUG>",
"query": "unresolved errors from the last 30 days",
"projectSlugOrId": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"limit": 25
}
2b. Cross-Reference with Test Coverage
For each Sentry error:
- Identify the source file from the stack trace
- Check if a corresponding test file exists (
Glob: **/<filename>.test.*) - If tests exist, check whether they cover the failing code path
- If no tests exist, prioritize writing tests for this file
2c. Generate Priority List
COVERAGE GAP ANALYSIS:
- Production errors without tests: [count]
1. [file] — [error type] — [frequency] — NO TEST FILE
2. [file] — [error type] — [frequency] — test exists but missing edge case
- Highest-impact files to test: [ordered list]
Step 3: Write Tests
Test Structure (AAA Pattern)
describe('ComponentOrModule', () => {
describe('methodOrBehavior', () => {
it('should [expected behavior] when [condition]', () => {
// Arrange — set up test data and dependencies
const input = createTestData();
// Act — execute the behavior under test
const result = functionUnderTest(input);
// Assert — verify the outcome
expect(result).toEqual(expectedOutput);
});
});
});
Naming Conventions
Test file names — co-locate with source or mirror directory structure:
| Source File | Test File |
|---|---|
src/utils/formatDate.ts | src/utils/formatDate.test.ts |
src/components/Button.tsx | src/components/Button.test.tsx |
app/services/user.py | tests/services/test_user.py |
Test descriptions — use should [behavior] when [condition]:
describe('validateEmail', () => {
it('should return true for valid email', () => { /* ... */ });
it('should return false when @ is missing', () => { /* ... */ });
it('should return false for empty string', () => { /* ... */ });
it('should handle unicode characters in local part', () => { /* ... */ });
});
What to Test
DO test:
- Public API / exported functions
- Business logic and domain rules
- Edge cases and boundary values
- Error handling and failure modes
- User interactions (clicks, inputs, submissions)
- State transitions
- Data transformations
DON'T test:
- Implementation details (private methods, internal state)
- Third-party library internals
- Trivial code (simple getters/setters)
- Framework behavior
- CSS classes or DOM structure (test behavior, not markup)
Edge Cases to Cover
For every function, consider:
| Category | Test Values |
|---|---|
| Empty | null, undefined, "", [], {} |
| Boundary | 0, -1, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER |
| Type confusion | String where number expected, array where object expected |
| Unicode | Emoji, CJK characters, RTL text, zero-width spaces |
| Concurrency | Rapid successive calls, race conditions |
| Network | Timeout, 404, 500, empty response, malformed JSON |
| Dates | Midnight, DST transitions, leap years, timezone boundaries |
Step 4: Testing Patterns by Category
Pure Functions
describe('formatCurrency', () => {
it('should format positive amounts', () => {
expect(formatCurrency(1234.5)).toBe('$1,234.50');
});
it('should handle zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('should handle negative amounts', () => {
expect(formatCurrency(-100)).toBe('-$100.00');
});
it('should handle very large numbers', () => {
expect(formatCurrency(999999999.99)).toBe('$999,999,999.99');
});
});
Async Functions
describe('fetchUser', () => {
it('should return user data for valid id', async () => {
const user = await fetchUser('123');
expect(user).toEqual({ id: '123', name: 'John Doe' });
});
it('should throw for non-existent user', async () => {
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});
it('should handle network timeout', async () => {
vi.useFakeTimers(); // or jest.useFakeTimers()
const promise = fetchUser('123');
vi.advanceTimersByTime(30000);
await expect(promise).rejects.toThrow('timeout');
vi.useRealTimers();
});
});
Mocking
// Vitest
import { vi } from 'vitest';
vi.mock('./emailService', () => ({
sendEmail: vi.fn().mockResolvedValue({ success: true }),
}));
// Jest
jest.mock('./emailService');
// MSW (API mocking — preferred for HTTP)
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({ id: params.id, name: 'Test User' });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
React Components (Testing Library)
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('LoginForm', () => {
it('should submit with valid credentials', async () => {
const onSubmit = vi.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/email/i), '[email protected]');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
});
});
});
it('should show validation error for invalid email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.type(screen.getByLabelText(/email/i), 'invalid');
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(screen.getByText(/valid email/i)).toBeInTheDocument();
});
it('should disable submit button while loading', () => {
render(<LoginForm onSubmit={vi.fn()} isLoading />);
expect(screen.getByRole('button', { name: /sign in/i })).toBeDisabled();
});
});
Custom Hooks
import { renderHook, act, waitFor } from '@testing-library/react';
describe('useCounter', () => {
it('should start with initial value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('should increment', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('should not go below zero', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.decrement());
expect(result.current.count).toBe(0);
});
});
API Route Handlers (Next.js / Node)
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
const req = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Test', email: '[email protected]' }),
});
const response = await POST(req);
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toMatchObject({ name: 'Test', email: '[email protected]' });
});
it('should return 422 for invalid email', async () => {
const req = new Request('http://localhost/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Test', email: 'invalid' }),
});
const response = await POST(req);
expect(response.status).toBe(422);
});
});
Step 5: Test Data Management
Use Factories
const createUser = (overrides: Partial<User> = {}): User => ({
id: crypto.randomUUID(),
name: 'Test User',
email: '[email protected]',
role: 'user',
createdAt: new Date('2024-01-01'),
...overrides,
});
// Usage
const admin = createUser({ role: 'admin' });
const inactive = createUser({ status: 'inactive', name: 'Inactive User' });
Use Meaningful Data
// Bad — meaningless
const data = { a: 'b', c: 'd' };
// Good — realistic and descriptive
const user = { name: 'Jane Doe', email: '[email protected]', role: 'admin' };