Testing Guide
Learn how to write and run tests for your SaaS application using Vitest and Playwright
Advanced Developer Content
This guide requires technical knowledge:
- Basic understanding of unit testing concepts
- Familiarity with TypeScript and async/await
- Knowledge of your application's codebase structure
The SaaS starter kit comes with Vitest for unit testing and Playwright for E2E testing. This guide shows you how to write and run tests for your custom features.
Testing Overview
The project includes two types of tests:
- Unit Tests: Test individual functions and utilities in isolation using Vitest
- E2E Tests: Test complete user flows and interactions using Playwright
Running Tests
Run Unit Tests
Execute all unit tests with Vitest:
1# Run tests in watch mode (interactive)2npm run test3 4# Run tests once (CI mode)5npm run test:run6 7# Run tests with coverage report8npm run test:coverageWatch Mode
h in the terminal to see all available commands.Run E2E Tests
Execute end-to-end tests with Playwright:
1# Run E2E tests headlessly2npm run test:e2e3 4# Run E2E tests with UI (interactive)5npm run test:e2e:uiCheck Test Results
Test output shows passed/failed tests:
1ā tests/unit/slug.test.ts (10 tests)2 ā slugify (10 tests)3 ā converts to lowercase4 ā replaces spaces with hyphens5 ā removes special characters6 7Test Files 1 passed (1)8 Tests 10 passed (10)9 Start at 10:30:4510 Duration 523msWriting Unit Tests
Create a Test File
Create test files in the tests/unit/ directory with .test.ts extension:
1# Create a test file for utility functions2touch tests/unit/myutils.test.tsWrite Your First Test
Here's a simple example testing a utility function:
1import { describe, it, expect } from "vitest";2import { slugify } from "@/lib/slug";3 4describe("slugify", () => {5 it("converts to lowercase", () => {6 expect(slugify("Hello World")).toBe("hello-world");7 });8 9 it("replaces spaces with hyphens", () => {10 expect(slugify("my workspace name")).toBe("my-workspace-name");11 });12 13 it("handles empty input", () => {14 expect(slugify("")).toBe("workspace");15 });16});Tests follow the AAA pattern: Arrange (setup), Act (execute), Assert (verify).
Test with Mocks
Mock external dependencies like database or API calls:
1import { describe, it, expect, beforeEach, vi } from 'vitest';2import { isAdmin } from '@/lib/admin';3 4// Mock the database5vi.mock('@/lib/db', () => ({6 prisma: {7 admin: {8 findUnique: vi.fn(),9 upsert: vi.fn(),10 },11 },12}));13 14describe('isAdmin', () => {15 beforeEach(() => {16 vi.clearAllMocks();17 });18 19 it('returns true for admin email', async () => {20 // Arrange21 const { prisma } = await import('@/lib/db');22 (prisma.admin.findUnique as any).mockResolvedValue({23 id: 'test-id',24 email: 'admin@example.com',25 });26 27 // Act28 const result = await isAdmin('admin@example.com');29 30 // Assert31 expect(result).toBe(true);32 });33});Mocking Strategy
vitest.config.ts. Database and auth mocks prevent tests from touching real services.Testing Best Practices
Test File Organization
1tests/2āāā unit/ # Unit tests3ā āāā admin.test.ts # Admin utilities4ā āāā slug.test.ts # Slug utilities5ā āāā stats.test.ts # Statistics6ā āāā billing/ # Billing tests7āāā e2e/ # End-to-end tests8 āāā auth.spec.ts # Authentication flows9 āāā dashboard.spec.ts # Dashboard interactionsWhat to Test
- Utility Functions: Pure functions with clear inputs/outputs (like
slugify) - Business Logic: Plan calculations, permission checks, data transformations
- API Routes: Request/response handling, authentication, error cases
- Database Operations: CRUD operations, relationships, constraints
What NOT to Test
- Third-party libraries (Stripe, Prisma, Next.js internals)
- React component rendering details (use E2E tests instead)
- Simple getters/setters with no logic
- Configuration files
Test Naming Convention
1// Good: Describes behavior clearly2it("returns empty array when no admins exist", async () => {3 // ...4});5 6// Bad: Too vague7it("works correctly", async () => {8 // ...9});Example: Testing Custom Features
Let's say you added a calculateDiscount function. Here's how to test it:
Write the Function
1export function calculateDiscount(2 price: number,3 discountPercent: number4): number {5 if (price < 0 || discountPercent < 0 || discountPercent > 100) {6 throw new Error("Invalid input");7 }8 return price * (1 - discountPercent / 100);9}Write Comprehensive Tests
1import { describe, it, expect } from "vitest";2import { calculateDiscount } from "@/lib/billing/discount";3 4describe("calculateDiscount", () => {5 it("applies 10% discount correctly", () => {6 expect(calculateDiscount(100, 10)).toBe(90);7 });8 9 it("applies 50% discount correctly", () => {10 expect(calculateDiscount(200, 50)).toBe(100);11 });12 13 it("returns original price with 0% discount", () => {14 expect(calculateDiscount(100, 0)).toBe(100);15 });16 17 it("handles decimal prices", () => {18 expect(calculateDiscount(99.99, 15)).toBeCloseTo(84.99, 2);19 });20 21 it("throws error for negative price", () => {22 expect(() => calculateDiscount(-100, 10)).toThrow("Invalid input");23 });24 25 it("throws error for discount > 100%", () => {26 expect(() => calculateDiscount(100, 150)).toThrow("Invalid input");27 });28});Run and Verify
1npm run test discount.test.ts2 3# Output:4ā calculateDiscount (6 tests)5 ā applies 10% discount correctly6 ā applies 50% discount correctly7 ā returns original price with 0% discount8 ā handles decimal prices9 ā throws error for negative price10 ā throws error for discount > 100%Configuration
Vitest Configuration
Test settings are configured in vitest.config.ts:
1import { defineConfig } from "vitest/config";2import react from "@vitejs/plugin-react";3import path from "path";4 5export default defineConfig({6 plugins: [react()],7 test: {8 environment: "node",9 include: ["tests/unit/**/*.test.ts"],10 exclude: ["node_modules", "tests/e2e"],11 globals: true,12 alias: {13 "@": path.resolve(__dirname, "./src"),14 },15 coverage: {16 provider: "v8",17 reporter: ["text", "json", "html"],18 include: ["src/lib/**/*.ts"],19 },20 },21});Troubleshooting
Tests Failing with Module Errors
- Verify path aliases in
vitest.config.tsmatchtsconfig.json - Check that imports use
@/prefix correctly - Ensure all dependencies are installed:
npm install
Mocks Not Working
- Place
vi.mock()calls at the top of the file, before imports - Use
vi.clearAllMocks()inbeforeEachto reset state - Import mocked module after defining the mock
Async Tests Timing Out
- Ensure async tests use
async/awaitproperly - Check for unresolved promises or missing
await - Increase timeout if needed:
it("test", async () => , 10000)
Related Guides
Next Steps
Now that you understand testing, you might want to:
- Write tests for your custom API routes
- Test your custom database models
- Follow TDD (Test-Driven Development) for new features