Testing Strategy
TinyKit Pro includes a comprehensive testing infrastructure using Vitest for unit and integration tests, and Playwright for end-to-end tests.
TinyKit Pro includes a comprehensive testing infrastructure using Vitest for unit and integration tests, and Playwright for end-to-end tests.
Test Infrastructure Overview
| Test Type | Framework | Config | Command |
|---|---|---|---|
| Backend Unit Tests | Vitest + convex-test | convex/vitest.config.ts | bun test:backend |
| Frontend Unit Tests | Vitest + Testing Library | vitest.config.ts | bun test:frontend |
| All Unit Tests | Vitest | - | bun test |
| E2E Tests | Playwright | playwright.config.ts | bun test:e2e |
Quick Start
# Run all tests
bun test
# Run backend tests (Convex functions)
bun test:backend
# Run frontend tests (React components)
bun test:frontend
# Run tests with watch mode
bun test:watch
# Run tests with UI
bun test:ui
# Run tests with coverage
bun test:coverage
bun test:backend:coverage
bun test:frontend:coverage
# Run E2E tests
bun test:e2e
bun test:e2e:uiBackend Testing (Convex Functions)
Configuration
Backend tests use the convex-test package with edge-runtime environment:
// convex/vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["**/*.test.ts"],
exclude: ["_generated/**", "node_modules/**"],
environment: "edge-runtime",
globals: true,
testTimeout: 10000,
server: {
deps: {
inline: ["convex-test"],
},
},
},
});Writing Backend Tests
Basic Query Test
import { convexTest } from "convex-test";
import { describe, it, expect } from "vitest";
import schema from "../schema";
import { modules } from "../test.setup";
describe("faqs/public/queries", () => {
it("lists active FAQs", async () => {
const t = convexTest(schema, modules);
// Seed test data
await t.run(async (ctx) => {
await ctx.db.insert("faqs", {
question: "Test question?",
answer: "Test answer",
isActive: true,
displayOrder: 1,
});
});
// Run query
const faqs = await t.query(api.faqs.public.queries.listActive, {});
expect(faqs).toHaveLength(1);
expect(faqs[0].question).toBe("Test question?");
});
});Testing Mutations with Authentication
import { convexTest } from "convex-test";
import { vi, describe, it, expect, beforeEach } from "vitest";
import schema from "../schema";
import { modules } from "../test.setup";
// Mock Better Auth user helpers
vi.mock("../users/helpers", async (importOriginal) => {
const original = await importOriginal<typeof import("../users/helpers")>();
return {
...original,
getCurrentUserId: vi.fn(),
getUserById: vi.fn(),
};
});
import { getCurrentUserId, getUserById } from "../users/helpers";
const mockedGetCurrentUserId = vi.mocked(getCurrentUserId);
const mockedGetUserById = vi.mocked(getUserById);
describe("users/private/mutations", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("updates user profile when authenticated", async () => {
const t = convexTest(schema, modules);
// Mock authenticated user
mockedGetCurrentUserId.mockResolvedValue("user_123");
mockedGetUserById.mockResolvedValue({
_id: "user_123",
name: "Test User",
email: "test@example.com",
role: "user",
emailVerified: true,
createdAt: Date.now(),
updatedAt: Date.now(),
});
await t.run(async (ctx) => {
vi.spyOn(ctx, "runQuery").mockResolvedValue(0);
// Test the mutation
await t.mutation(api.users.private.mutations.updateProfile, {
name: "Updated Name",
});
});
// Verify changes...
});
it("throws when not authenticated", async () => {
const t = convexTest(schema, modules);
mockedGetCurrentUserId.mockResolvedValue(null);
await t.run(async (ctx) => {
await expect(
t.mutation(api.users.private.mutations.updateProfile, {
name: "Test",
}),
).rejects.toThrow("Authentication required");
});
});
});Testing Access Control
import { convexTest } from "convex-test";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { requireAccess, hasAccess } from "./requireAccess";
describe("access control", () => {
it("allows admin access to admin-only functions", async () => {
const t = convexTest(schema, modules);
mockedGetCurrentUserId.mockResolvedValue("admin_123");
mockedGetUserById.mockResolvedValue({
_id: "admin_123",
role: "admin",
// ... other fields
});
await t.run(async (ctx) => {
vi.spyOn(ctx, "runQuery").mockResolvedValue(0);
const result = await requireAccess(ctx, { userRole: ["admin"] });
expect(result.userRole).toBe("admin");
});
});
it("denies non-admin access to admin functions", async () => {
const t = convexTest(schema, modules);
mockedGetCurrentUserId.mockResolvedValue("user_123");
mockedGetUserById.mockResolvedValue({
_id: "user_123",
role: "user",
// ... other fields
});
await t.run(async (ctx) => {
vi.spyOn(ctx, "runQuery").mockResolvedValue(0);
await expect(requireAccess(ctx, { userRole: ["admin"] })).rejects.toThrow(
"Required user role: admin",
);
});
});
});Test Fixtures
Create reusable test fixtures for consistent test data:
// convex/test.fixtures.ts
export const TEST_USERS = {
admin: {
subject: "admin_123",
email: "admin@example.com",
role: "admin",
},
regularUser: {
subject: "user_123",
email: "user@example.com",
role: "user",
},
orgOwner: {
subject: "owner_123",
email: "owner@example.com",
role: "user",
},
};
export const MOCK_BETTER_AUTH_USERS = {
admin: {
_id: TEST_USERS.admin.subject,
name: "Admin User",
email: TEST_USERS.admin.email,
role: "admin",
emailVerified: true,
createdAt: Date.now(),
updatedAt: Date.now(),
},
regularUser: {
_id: TEST_USERS.regularUser.subject,
name: "Regular User",
email: TEST_USERS.regularUser.email,
role: "user",
emailVerified: true,
createdAt: Date.now(),
updatedAt: Date.now(),
},
};Backend Mock Utilities
TinyKit Pro provides comprehensive mock utilities for external services in convex/test.mocks.ts:
Stripe Mocks
import {
mockStripe,
createMockStripeCustomer,
createMockStripeSubscription,
createMockCheckoutSession,
} from "./test.mocks";
// Mock Stripe in tests
vi.mock("@convex-dev/stripe", () => ({
StripeSubscriptions: mockStripe.StripeSubscriptions,
}));
describe("billing functions", () => {
it("creates checkout session", async () => {
const t = convexTest(schema, modules);
// Use mock factory with custom data
const mockSession = createMockCheckoutSession({
customer: "cus_test_123",
url: "https://checkout.stripe.com/custom",
});
// Test billing functions...
});
});Resend Email Mocks
import { mockResend, createMockResendResponse } from "./test.mocks";
// Mock Resend in tests
vi.mock("@convex-dev/resend", () => ({
Resend: mockResend.Resend,
}));
describe("email notifications", () => {
it("sends email successfully", async () => {
const t = convexTest(schema, modules);
// Mock will return { id: "email_..." } automatically
await t.action(api.emails.sendWelcomeEmail, {
to: "user@example.com",
name: "Test User",
});
expect(mockResend.Resend).toHaveBeenCalled();
});
});Storage Mocks
import {
mockConvexStorage,
createMockStorageId,
createMockStorageUrl,
} from "./test.mocks";
describe("file operations", () => {
it("generates storage URL", async () => {
const t = convexTest(schema, modules);
await t.run(async (ctx) => {
// Mock storage operations
const storageId = createMockStorageId();
const url = await mockConvexStorage.getUrl(storageId);
expect(url).toMatch(/^https:\/\/convex\.cloud\/storage\//);
});
});
});Complete Context Mocking
import { createMockConvexContext } from "./test.mocks";
describe("functions with storage and auth", () => {
it("processes user files", async () => {
const t = convexTest(schema, modules);
// Create complete mock context
const mockCtx = createMockConvexContext({
user: {
_id: "user_123",
email: "test@example.com",
role: "user",
name: "Test User",
},
storageUrls: {
kg_storage_123: "https://custom-url.com/file.jpg",
},
});
// Use in tests...
});
});Frontend Testing (React Components)
Configuration
Frontend tests use happy-dom environment with React Testing Library:
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "happy-dom",
globals: true,
setupFiles: ["./src/test/setup.tsx"],
include: ["src/**/*.test.{ts,tsx}"],
exclude: ["**/node_modules/**", "convex/**", "e2e/**"],
},
});Writing Frontend Tests
Component Test Example
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { Button } from "@/components/ui/button";
describe("Button component", () => {
it("renders with correct text", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button")).toHaveTextContent("Click me");
});
it("calls onClick handler when clicked", () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
});Testing with Convex Hooks
TinyKit Pro provides pre-built mock utilities for Convex React hooks in src/test/mocks/:
import { render, screen, waitFor } from "@testing-library/react";
import { vi, describe, it, expect, beforeEach } from "vitest";
import {
mockUseQuery,
mockUseMutation,
mockUsePaginatedQuery,
setQueryLoaded,
setQueryLoading,
resetConvexMocks,
} from "@/test/mocks";
// Mock Convex hooks with pre-built mocks
vi.mock("convex/react", () => ({
useQuery: mockUseQuery,
useMutation: mockUseMutation,
usePaginatedQuery: mockUsePaginatedQuery,
}));
describe("UserProfile component", () => {
beforeEach(() => {
resetConvexMocks(); // Clean state between tests
});
it("displays user name when loaded", async () => {
// Use helper to set loaded state
setQueryLoaded({
name: "Test User",
email: "test@example.com",
});
render(<UserProfile />);
await waitFor(() => {
expect(screen.getByText("Test User")).toBeInTheDocument();
});
});
it("shows loading state", () => {
// Use helper to set loading state
setQueryLoading();
render(<UserProfile />);
expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument();
});
it("handles mutation", async () => {
const updateProfile = mockUseMutation();
render(<ProfileForm />);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(updateProfile).toHaveBeenCalledWith({
name: "Updated Name",
});
});
});
});Testing with Better Auth
import { render, screen, waitFor } from "@testing-library/react";
import { vi, describe, it, expect } from "vitest";
import {
mockAuthClient,
mockAuthClientLoggedOut,
createMockAuthClient,
createMockUser,
} from "@/test/mocks";
// Mock Better Auth client
vi.mock("@/lib/auth-client", () => ({
authClient: mockAuthClient,
}));
describe("AuthenticatedComponent", () => {
it("shows user info when authenticated", async () => {
render(<UserMenu />);
await waitFor(() => {
expect(screen.getByText("Test User")).toBeInTheDocument();
expect(screen.getByText("test@example.com")).toBeInTheDocument();
});
});
it("shows sign in prompt when logged out", () => {
// Use logged out variant
vi.mocked(mockAuthClient).getSession.mockResolvedValue(null);
vi.mocked(mockAuthClient).useSession.mockReturnValue({
data: null,
isPending: false,
error: null,
});
render(<UserMenu />);
expect(screen.getByText("Sign In")).toBeInTheDocument();
});
it("handles custom user", () => {
// Create custom mock with specific user data
const customClient = createMockAuthClient({
id: "admin_123",
email: "admin@example.com",
role: "admin",
name: "Admin User",
});
vi.mocked(mockAuthClient).getSession = customClient.getSession;
render(<AdminPanel />);
expect(screen.getByText("Admin User")).toBeInTheDocument();
});
});Frontend Mock Utilities Reference
All frontend mocks are exported from @/test/mocks for easy import:
Convex Mocks
import {
// Core mocks
mockUseQuery,
mockUseMutation,
mockUseAction,
mockUsePaginatedQuery,
mockUseConvex,
// Helpers
setQueryLoading,
setQueryLoaded,
setMutationError,
setPaginatedResults,
resetConvexMocks,
// Factories
createMockQueryFn,
createMockMutationFn,
} from "@/test/mocks";Better Auth Mocks
import {
// Types
type MockUser,
type MockSession,
// Factories
createMockUser,
createMockSession,
createMockAuthClient,
// Complete mock client
mockAuthClient,
// Variants
mockAuthClientLoggedOut,
mockAuthClientLoading,
// Utilities
resetAuthMocks,
} from "@/test/mocks";E2E Testing (Playwright)
Running E2E Tests
# Run all E2E tests
bun test:e2e
# Run with UI mode
bun test:e2e:ui
# Run specific test file
bunx playwright test e2e/auth.spec.ts
# Run in headed mode (see browser)
bunx playwright test --headedWriting E2E Tests
// e2e/auth.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Authentication", () => {
test("user can sign in", async ({ page }) => {
await page.goto("/auth/sign-in");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
// Should redirect to home
await expect(page).toHaveURL("/home");
await expect(page.getByText("Dashboard")).toBeVisible();
});
test("shows error for invalid credentials", async ({ page }) => {
await page.goto("/auth/sign-in");
await page.fill('input[name="email"]', "wrong@example.com");
await page.fill('input[name="password"]', "wrongpassword");
await page.click('button[type="submit"]');
await expect(page.getByText(/invalid credentials/i)).toBeVisible();
});
});Test Coverage
TinyKit Pro enforces 80% coverage thresholds for all code (lines, functions, branches, statements).
Coverage Requirements
Both frontend and backend tests enforce these thresholds in CI/CD:
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
}CI/CD Integration: Tests will fail in CI if coverage drops below 80% for any metric.
Viewing Coverage Reports
# Generate coverage for all tests
bun test:coverage
# Generate coverage for backend only
bun test:backend:coverage
# Generate coverage for frontend only
bun test:frontend:coverageCoverage reports are generated in:
- Backend:
convex/coverage/directory - Frontend:
coverage/directory
Open coverage/index.html in a browser to view detailed HTML reports.
Coverage Exclusions
Backend (from convex/vitest.config.ts):
convex/_generated/**- Generated filesconvex/betterAuth/_generated/**- Better Auth generatedconvex/betterAuth/generatedSchema.ts- Generated schemaconvex/**/schema.ts- Schema definitions (type-only)convex/**/validators.ts- Validator definitions (type-only)convex/**/*.test.ts- Test filesconvex/test.*.ts- Test utilitiesconvex/convex.config.ts- Config fileconvex/auth.config.ts- Auth configconvex/files.ts- Simple utilitiesconvex/migrations.ts- Migration files
Frontend (from vitest.config.ts):
src/components/ui/**- shadcn/ui components (external)src/components/magicui/**- MagicUI components (external)src/test/**- Test setup and mocks**/*.test.{ts,tsx}- Test filessrc/app/**/page.tsx- Next.js pages (tested via E2E)src/app/**/layout.tsx- Next.js layouts (tested via E2E)
Best Practices
Backend Testing
- Mock Better Auth: Always mock user helpers when testing authenticated functions
- Use Test Fixtures: Create reusable fixtures for consistent test data
- Test Access Control: Verify both success and failure paths for authorization
- Isolate Tests: Each test should be independent and not rely on state from other tests
Frontend Testing
- Test User Behavior: Focus on what users see and do, not implementation details
- Use Semantic Queries: Prefer
getByRole,getByTextovergetByTestId - Mock External Dependencies: Mock Convex hooks and API calls
- Test Loading/Error States: Verify UI handles loading and error conditions
E2E Testing
- Test Critical Paths: Focus on most important user journeys
- Use Page Objects: Create reusable page object patterns for common interactions
- Handle Async Operations: Use proper waitFor and assertions
- Clean Test Data: Ensure tests don't leave behind data that affects other tests
Debugging Tests
Backend Tests
# Run with verbose output
bun test:backend -- --reporter=verbose
# Run specific test
bun test:backend -- --grep "allows admin access"
# Debug mode
bun test:backend -- --inspect-brkFrontend Tests
# Run with UI for debugging
bun test:ui
# Run specific file
bun test:frontend -- src/components/Button.test.tsxE2E Tests
# Debug mode with browser visible
bunx playwright test --headed --debug
# Trace viewer for failed tests
bunx playwright show-trace trace.zipCI Integration
Tests run automatically in CI. Example GitHub Actions workflow:
- name: Run Tests
run: |
bun test
bun test:e2eDatabase Triggers
TinyKit Pro uses convex-helpers/server/triggers to automatically maintain data integrity and consistency across database operations. The trigger system inter...
Project Architecture
TinyKit Pro is built with a modern, scalable architecture using cutting-edge technologies for real-time collaboration and organization productivity.