From jm-claude-plugin
Testing guide for Vitest, React Testing Library, and Supabase mocks. Use this when writing unit or integration tests, setting up test infrastructure, mocking Supabase, testing hooks/components, or deciding what to test first. Trigger on: "add tests", "how do I test this?", "mock supabase", any new feature/hook/service.
How this skill is triggered — by the user, by Claude, or both
Slash command
/jm-claude-plugin:code-testThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Staff Engineer standard: tests prove **behavior**, not implementation.
references/advanced-environments.mdreferences/advanced-projects.mdreferences/advanced-type-testing.mdreferences/advanced-vi.mdreferences/core-cli.mdreferences/core-config.mdreferences/core-describe.mdreferences/core-expect.mdreferences/core-hooks.mdreferences/core-test-api.mdreferences/features-concurrency.mdreferences/features-context.mdreferences/features-coverage.mdreferences/features-filtering.mdreferences/features-mocking.mdreferences/features-snapshots.mdStaff Engineer standard: tests prove behavior, not implementation.
1. Utils & validators (pure functions — easiest, highest value)
2. Zod schemas (safeParse edge cases)
3. Service functions (business logic)
4. Custom hooks (useAuth, useForm)
5. Components (user interactions)
6. API integrations (Supabase queries)
tests/
├── unit/
│ ├── utils/
│ │ └── validators.test.ts
│ └── services/
│ └── auth.test.ts
├── integration/
│ └── api/
│ └── users.test.ts
└── e2e/ → See e2etesting skill
└── specs/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { validateEmail } from "@/utils/validators";
// ✅ GOOD — descriptive names, one assertion per test, AAA pattern
describe("validateEmail", () => {
it("returns true for valid email format", () => {
expect(validateEmail("[email protected]")).toBe(true);
});
it("returns false when missing @ symbol", () => {
expect(validateEmail("invalid-email")).toBe(false);
});
it("returns false for empty string", () => {
expect(validateEmail("")).toBe(false);
});
});
// ❌ BAD — vague name, multiple assertions, no structure
it("email test", () => {
expect(validateEmail("[email protected]")).toBe(true);
expect(validateEmail("bad")).toBe(false);
expect(validateEmail("")).toBe(false);
});
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "@/components/LoginForm";
describe("LoginForm", () => {
const mockOnSubmit = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
it("submits with email and password", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText(/email/i), "[email protected]");
await user.type(screen.getByLabelText(/password/i), "securePass123");
await user.click(screen.getByRole("button", { name: /sign in/i }));
expect(mockOnSubmit).toHaveBeenCalledWith({
email: "[email protected]",
password: "securePass123",
});
});
it("shows error when fields are empty", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.click(screen.getByRole("button", { name: /sign in/i }));
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(mockOnSubmit).not.toHaveBeenCalled();
});
});
Query priority (Testing Library):
1. getByRole → accessible, resilient
2. getByLabelText → form elements
3. getByText → static content
4. getByTestId → last resort (data-testid)
// tests/mocks/supabase.ts
import { vi } from "vitest";
export const mockSupabase = {
from: vi.fn().mockReturnThis(),
select: vi.fn().mockReturnThis(),
insert: vi.fn().mockReturnThis(),
update: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
eq: vi.fn().mockReturnThis(),
single: vi.fn(),
auth: {
signInWithPassword: vi.fn(),
signUp: vi.fn(),
signOut: vi.fn(),
getSession: vi.fn(),
onAuthStateChange: vi.fn(() => ({
data: { subscription: { unsubscribe: vi.fn() } },
})),
},
};
vi.mock("@/lib/supabase", () => ({
supabase: mockSupabase,
}));
// Usage in test
import { mockSupabase } from "../mocks/supabase";
it("fetches user by id", async () => {
mockSupabase.single.mockResolvedValueOnce({
data: { id: "1", email: "[email protected]" },
error: null,
});
const user = await getUser("1");
expect(user.email).toBe("[email protected]");
});
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "@/hooks/useCounter";
it("increments counter", () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
✅ DO:
- Mock external services (Supabase, APIs, PostHog)
- Test behavior, not implementation details
- Keep tests independent (no shared state)
- Aim for 70%+ coverage on critical paths
- Use vi.clearAllMocks() in beforeEach
❌ DON'T:
- Test implementation details (internal state, private methods)
- Share state between tests
- Use production data
- Test third-party library behavior
- Snapshot test everything (snapshots are brittle)
references/ contains the full Vitest 3.x API reference library — 16 files covering:
mocking, coverage, snapshot testing, browser mode, workspace config, CLI flags,
TypeScript integration, watch mode, and migration from Jest.
[ ] Tests follow AAA pattern (Arrange, Act, Assert)
[ ] Descriptive test names explain the behavior
[ ] Supabase/external services properly mocked
[ ] No shared state between tests
[ ] Critical paths have tests (auth, payments, data mutations)
[ ] All tests pass: pnpm test
Provides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub jjmendezrodriguez/jm-claude-plugin --plugin jm-claude-plugin