Test coverage and quality review — what needs tests, test quality assessment, anti-patterns, and the testing pyramid for Next.js + Supabase applications.
// ❌ Testing mocks, not code — this test will pass even if fetchUsers is broken
vi.mock('@/lib/api', () => ({
fetchUsers: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }])
}))
it('displays users', async () => {
render(<UserList />)
await waitFor(() => expect(screen.getByText('Test')).toBeInTheDocument())
})
// This tells you the component renders mock data, not that your API integration works
// ✅ Integration test with real data layer (test database)
it('displays users from the database', async () => {
await seedTestDatabase([{ id: 1, name: 'Test' }])
render(<UserList />)
await waitFor(() => expect(screen.getByText('Test')).toBeInTheDocument())
})
Date.now() or new Date() in assertions (use vi.useFakeTimers())
setTimeout races (use vi.advanceTimersByTime())
Order-dependent tests (test B depends on state from test A)
Network calls to real APIs (mock them or use MSW)
Anti-Pattern 6: Array Index as Key
// Not a test issue, but flag in component tests:
// ❌ Causes bugs on reorder, delete, insert
{items.map((item, i) => <ListItem key={i} item={item} />)}
// ✅ Stable identity
{items.map(item => <ListItem key={item.id} item={item} />)}
Testing Architecture for Our Stack
The Pyramid
┌──────┐
│ E2E │ Playwright: 5-10 critical user journeys
│ │ Login → Create → View → Edit → Delete
├──────┤
│ Integ│ Vitest + real DB: API routes, Server Actions,
│ation │ data layer, auth flows (20-50 tests)
├──────┤
│ Unit │ Vitest: pure functions, utils, hooks,
│ │ validation schemas, transforms (100+ tests)
└──────┘
Tool Mapping
Layer
Tool
What to Test
Unit
Vitest
Pure functions, Zod schemas, transforms, hooks
Integration
Vitest + Supabase test project
API routes, Server Actions, DB queries, RLS policies
E2E
Playwright
Full user journeys: signup → use feature → logout
Component
Vitest + Testing Library
User interaction, form submission, error states
React Testing Library Philosophy (from the repo)
"The more your tests resemble the way your software is used, the more confidence they can give you."
Query by role, label, text — not by class, ID, or test-id
Fire real events (userEvent.click) not synthetic (fireEvent.click)
Assert on what the user sees, not on component internals
If you can't test something without accessing component internals, your component API needs refactoring
Database/RLS Test Patterns
// Test RLS policies directly
describe('RLS: users table', () => {
it('user can only read own profile', async () => {
const userA = await createTestUser()
const userB = await createTestUser()
const clientA = createClientAs(userA)
const { data } = await clientA.from('profiles').select('*')
expect(data).toHaveLength(1)
expect(data[0].id).toBe(userA.id)
// userB's profile is not visible
})
it('unauthenticated user cannot read profiles', async () => {
const anonClient = createAnonClient()
const { data, error } = await anonClient.from('profiles').select('*')
expect(data).toHaveLength(0) // RLS blocks everything
})
})
Sources
testing-library/react-testing-library documentation and guiding principles
vitest-dev/vitest configuration and best practices
Kent C. Dodds: Testing Implementation Details
Supabase testing documentation
Changelog
2026-03-21: Initial skill — test review for Next.js + Supabase
1---2name: testing-review3description: Test coverage and quality review — what needs tests, test quality assessment, anti-patterns, and the testing pyramid for Next.js + Supabase applications.4---56# Testing Review78## Quick Reference — PR Test Checklist910**When reviewing a PR, check:**111. [ ] Every new function with branching logic has a test122. [ ] Every new API route has at least one happy-path + one error-path test133. [ ] Every new Server Action has validation tested144. [ ] Financial/payment logic has comprehensive tests (P0 if missing)155. [ ] Auth flows have test coverage166. [ ] Edge cases covered: empty arrays, null inputs, boundary values177. [ ] Tests are deterministic (no time-dependent, no random, no network)188. [ ] Test descriptions describe behavior, not implementation1920---2122## What to Flag When Tests Are Missing2324### P0 — Block merge without tests:25- Payment/financial logic (transfers, billing, coin transactions)26- Authentication flows (login, signup, token refresh, logout)27- Authorization checks (RLS policies, ownership verification)28- Data migrations that modify existing data2930### P1 — Should have tests:31- Complex business logic with branching (>2 branches)32- API routes with input validation33- Server Actions that mutate data34- Utility functions used in >2 places35- Error handling paths3637### OK without tests (don't flag):38- Pure UI components (just layout/styling, no logic)39- Type-only files (`types.ts`, `interfaces.ts`)40- Configuration files (`next.config.ts`, `tailwind.config.ts`)41- One-line utility functions that are trivially correct42- Generated code (Supabase types, migration files)4344---4546## Test Quality Review4748### Anti-Pattern 1: Testing Implementation, Not Behavior49```ts50// ❌ Tests internal state — breaks if you refactor51it('sets loading to true', () => {52 const { result } = renderHook(() => useUsers())53 expect(result.current.loading).toBe(true)54})5556// ✅ Tests behavior — survives refactors57it('shows loading spinner while fetching users', async () => {58 render(<UserList />)59 expect(screen.getByRole('progressbar')).toBeInTheDocument()60 await waitFor(() => {61 expect(screen.getByText('John Doe')).toBeInTheDocument()62 })63})64```6566### Anti-Pattern 2: Tests with No Assertions67```ts68// ❌ Passes but verifies NOTHING69it('renders without crashing', () => {70 render(<Dashboard />)71})7273// ✅ Actually verifies something74it('renders the dashboard title', () => {75 render(<Dashboard />)76 expect(screen.getByRole('heading', { name: 'Dashboard' })).toBeInTheDocument()77})78```7980### Anti-Pattern 3: Over-Mocking81```ts82// ❌ Testing mocks, not code — this test will pass even if fetchUsers is broken83vi.mock('@/lib/api', () => ({84 fetchUsers: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }])85}))86it('displays users', async () => {87 render(<UserList />)88 await waitFor(() => expect(screen.getByText('Test')).toBeInTheDocument())89})90// This tells you the component renders mock data, not that your API integration works9192// ✅ Integration test with real data layer (test database)93it('displays users from the database', async () => {94 await seedTestDatabase([{ id: 1, name: 'Test' }])95 render(<UserList />)96 await waitFor(() => expect(screen.getByText('Test')).toBeInTheDocument())97})98```99100### Anti-Pattern 4: Only Happy Path101```ts102// ❌ Only tests success case103it('creates a user', async () => {104 const result = await createUser({ name: 'John', email: 'john@test.com' })105 expect(result.success).toBe(true)106})107108// ✅ Tests error cases too109it('rejects invalid email', async () => {110 const result = await createUser({ name: 'John', email: 'not-an-email' })111 expect(result.success).toBe(false)112 expect(result.error).toContain('email')113})114115it('rejects duplicate email', async () => {116 await createUser({ name: 'John', email: 'john@test.com' })117 const result = await createUser({ name: 'Jane', email: 'john@test.com' })118 expect(result.success).toBe(false)119})120121it('handles database errors gracefully', async () => {122 // Simulate DB failure123 vi.spyOn(supabase, 'from').mockRejectedValueOnce(new Error('connection lost'))124 const result = await createUser({ name: 'John', email: 'john@test.com' })125 expect(result.success).toBe(false)126 expect(result.error).not.toContain('connection lost') // no internal details leaked127})128```129130### Anti-Pattern 5: Flaky Tests131Sources of flakiness:132- `Date.now()` or `new Date()` in assertions (use `vi.useFakeTimers()`)133- `setTimeout` races (use `vi.advanceTimersByTime()`)134- Order-dependent tests (test B depends on state from test A)135- Network calls to real APIs (mock them or use MSW)136137### Anti-Pattern 6: Array Index as Key138```tsx139// Not a test issue, but flag in component tests:140// ❌ Causes bugs on reorder, delete, insert141{items.map((item, i) => <ListItem key={i} item={item} />)}142143// ✅ Stable identity144{items.map(item => <ListItem key={item.id} item={item} />)}145```146147---148149## Testing Architecture for Our Stack150151### The Pyramid152```153 ┌──────┐154 │ E2E │ Playwright: 5-10 critical user journeys155 │ │ Login → Create → View → Edit → Delete156 ├──────┤157 │ Integ│ Vitest + real DB: API routes, Server Actions,158 │ation │ data layer, auth flows (20-50 tests)159 ├──────┤160 │ Unit │ Vitest: pure functions, utils, hooks,161 │ │ validation schemas, transforms (100+ tests)162 └──────┘163```164165### Tool Mapping166| Layer | Tool | What to Test |167|-------|------|-------------|168| Unit | Vitest | Pure functions, Zod schemas, transforms, hooks |169| Integration | Vitest + Supabase test project | API routes, Server Actions, DB queries, RLS policies |170| E2E | Playwright | Full user journeys: signup → use feature → logout |171| Component | Vitest + Testing Library | User interaction, form submission, error states |172173### React Testing Library Philosophy (from the repo)174> "The more your tests resemble the way your software is used, the more confidence they can give you."175176- Query by role, label, text — not by class, ID, or test-id177- Fire real events (`userEvent.click`) not synthetic (`fireEvent.click`)178- Assert on what the user sees, not on component internals179- If you can't test something without accessing component internals, your component API needs refactoring180181---182183## Database/RLS Test Patterns184185```ts186// Test RLS policies directly187describe('RLS: users table', () => {188 it('user can only read own profile', async () => {189 const userA = await createTestUser()190 const userB = await createTestUser()191192 const clientA = createClientAs(userA)193 const { data } = await clientA.from('profiles').select('*')194195 expect(data).toHaveLength(1)196 expect(data[0].id).toBe(userA.id)197 // userB's profile is not visible198 })199200 it('unauthenticated user cannot read profiles', async () => {201 const anonClient = createAnonClient()202 const { data, error } = await anonClient.from('profiles').select('*')203 expect(data).toHaveLength(0) // RLS blocks everything204 })205})206```207208## Sources209- testing-library/react-testing-library documentation and guiding principles210- vitest-dev/vitest configuration and best practices211- Kent C. Dodds: Testing Implementation Details212- Supabase testing documentation213214## Changelog215- 2026-03-21: Initial skill — test review for Next.js + Supabase
Run npx skillmds@latest add nickgallick/testing-review in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Test coverage and quality review — what needs tests, test quality assessment, anti-patterns, and the testing pyramid for Next.js + Supabase applications. It is listed under Web & Frontend on SkillMD.
SkillMD's automated safety review verdict for this skill is PASS. Independent scanners report: SkillSpector: PASS, Skill Scanner: PASS. Capability flags: reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
nickgallick (@nickgallick) published this skill. Their other Agent Skills are listed on their SkillMD profile.