Documentation Index Fetch the complete documentation index at: https://mintlify.com/formbricks/formbricks/llms.txt
Use this file to discover all available pages before exploring further.
This guide outlines our testing standards and best practices for maintaining high-quality code.
Testing Philosophy
Test What Matters Focus on business logic and user flows
Fast Feedback Tests should run quickly
Maintainable Easy to update as code evolves
Testing Strategy
Formbricks uses a multi-layered testing approach:
Unit Tests (Vitest)
Test individual functions and utilities in .ts files.
Use for:
Business logic
Utility functions
Data transformations
Service layer methods
Do not test:
.tsx files (React components)
Simple getters/setters
External library code
E2E Tests (Playwright)
Test complete user workflows through the browser.
Use for:
Critical user journeys
React component interactions
Full-stack flows
Cross-browser compatibility
Integration Tests
Test interactions between multiple modules.
Use for:
API endpoint testing
Database operations
Service integrations
Unit Testing with Vitest
Setup
Vitest is configured at the workspace and package levels:
# Run all unit tests
pnpm test
# Run with coverage
pnpm test:coverage
# Run in watch mode (during development)
pnpm test --watch
# Run specific test file
pnpm test path/to/test.test.ts
Test File Structure
Place tests next to the code they test:
lib/
survey/
service.ts
service.test.ts # Test file
utils.ts
utils.test.ts # Test file
Writing Tests
import { describe , it , expect } from "vitest" ;
import { calculateScore } from "./utils" ;
describe ( "calculateScore" , () => {
it ( "should calculate total score from responses" , () => {
const responses = [
{ score: 5 },
{ score: 3 },
{ score: 7 },
];
const result = calculateScore ( responses );
expect ( result ). toBe ( 15 );
});
it ( "should return 0 for empty responses" , () => {
const result = calculateScore ([]);
expect ( result ). toBe ( 0 );
});
});
Test Organization
Follow the Arrange-Act-Assert pattern:
it ( "should filter active surveys" , () => {
// Arrange - Set up test data
const surveys = [
{ id: "1" , status: "active" },
{ id: "2" , status: "draft" },
{ id: "3" , status: "active" },
];
// Act - Execute the function
const result = filterActiveSurveys ( surveys );
// Assert - Verify the result
expect ( result ). toHaveLength ( 2 );
expect ( result [ 0 ]. id ). toBe ( "1" );
expect ( result [ 1 ]. id ). toBe ( "3" );
});
Mocking
Mock Modules
import { vi } from "vitest" ;
vi . mock ( "@formbricks/database" , () => ({
prisma: {
survey: {
findMany: vi . fn (),
create: vi . fn (),
},
},
}));
Mock Functions
import { vi } from "vitest" ;
const mockCallback = vi . fn ();
mockCallback . mockReturnValue ( 42 );
mockCallback . mockResolvedValue ({ data: "test" });
Mock Implementation
vi . mocked ( prisma . survey . create ). mockImplementation ( async ( data ) => {
return {
id: "generated-id" ,
... data ,
};
});
Testing Async Code
import { describe , it , expect } from "vitest" ;
describe ( "async function" , () => {
it ( "should fetch survey data" , async () => {
const survey = await getSurvey ( "survey-1" );
expect ( survey ). toBeDefined ();
expect ( survey . id ). toBe ( "survey-1" );
});
it ( "should reject with error for invalid ID" , async () => {
await expect ( getSurvey ( "invalid" )). rejects . toThrow ( "Survey not found" );
});
});
Testing with Testing Library
For utilities that manipulate React elements:
import { render , screen } from "@testing-library/react" ;
import { describe , it , expect } from "vitest" ;
describe ( "Component utility" , () => {
it ( "should transform component props" , () => {
const transformed = transformProps ({ name: "Test" });
expect ( transformed ). toHaveProperty ( "displayName" , "Test" );
});
});
E2E Testing with Playwright
Setup
Playwright tests are located in apps/web/playwright/:
# Run E2E tests
pnpm test:e2e
# Run in UI mode (for debugging)
px playwright test --ui
# Run specific test
px playwright test billing.spec.ts
Test File Structure
apps/web/playwright/
auth/
login.spec.ts
signup.spec.ts
survey/
create-survey.spec.ts
edit-survey.spec.ts
billing.spec.ts
Writing E2E Tests
import { test , expect } from "@playwright/test" ;
test . describe ( "Survey Creation" , () => {
test . beforeEach ( async ({ page }) => {
// Login before each test
await page . goto ( "/auth/login" );
await page . fill ( '[name="email"]' , "test@example.com" );
await page . fill ( '[name="password"]' , "password" );
await page . click ( 'button[type="submit"]' );
await page . waitForURL ( "/" );
});
test ( "should create a new survey" , async ({ page }) => {
// Navigate to surveys
await page . goto ( "/surveys" );
// Click create button
await page . click ( 'text="Create Survey"' );
// Fill form
await page . fill ( '[name="name"]' , "My Test Survey" );
await page . fill ( '[name="description"]' , "Test description" );
// Submit
await page . click ( 'button[type="submit"]' );
// Verify creation
await expect ( page . locator ( 'text="Survey created"' )). toBeVisible ();
await expect ( page . locator ( 'text="My Test Survey"' )). toBeVisible ();
});
});
Playwright Best Practices
Use data-testid for stable selectors
// In component
< button data - testid = "create-survey-btn" > Create </ button >
// In test
await page . click ( '[data-testid="create-survey-btn"]' );
await page . click ( 'text="Submit"' );
await page . waitForURL ( "/success" );
class SurveyPage {
constructor ( private page : Page ) {}
async createSurvey ( name : string ) {
await this . page . click ( '[data-testid="create-btn"]' );
await this . page . fill ( '[name="name"]' , name );
await this . page . click ( '[type="submit"]' );
}
}
test ( "complex workflow @slow" , async ({ page }) => {
// Long-running test
});
Descriptive Test Names
Use descriptive filenames:
billing.spec.ts - Billing tests
survey-creation.spec.ts - Survey creation flow
user-settings.spec.ts - User settings
Code Coverage
Running Coverage
Coverage Goals
Minimum : 70% overall coverage
Critical paths : 90%+ coverage
New features : Must include tests
Viewing Coverage
Coverage reports are generated in coverage/ directory:
# Open HTML report
open coverage/index.html
What to Cover
Cover
Business logic
Data transformations
Validation functions
Service methods
Error handling
Edge cases
Don't Cover
Simple getters/setters
Type definitions
External libraries
Generated code
Configuration files
Testing Patterns
Test Data Builders
Create reusable test data builders:
class SurveyBuilder {
private data : Partial < Survey > = {};
withName ( name : string ) {
this . data . name = name ;
return this ;
}
withStatus ( status : SurveyStatus ) {
this . data . status = status ;
return this ;
}
build () : Survey {
return {
id: "survey-1" ,
name: "Test Survey" ,
status: "draft" ,
... this . data ,
};
}
}
// Usage
const survey = new SurveyBuilder ()
. withName ( "Custom Survey" )
. withStatus ( "active" )
. build ();
Test Fixtures
Use fixtures for common test data:
// fixtures/survey.ts
export const mockSurvey = {
id: "survey-1" ,
name: "Test Survey" ,
status: "active" ,
};
export const mockResponses = [
{ id: "r1" , surveyId: "survey-1" , score: 5 },
{ id: "r2" , surveyId: "survey-1" , score: 3 },
];
// In test
import { mockSurvey } from "./fixtures/survey" ;
Parameterized Tests
Test multiple scenarios:
import { describe , it , expect } from "vitest" ;
describe ( "validateEmail" , () => {
const validEmails = [
"test@example.com" ,
"user+tag@domain.co.uk" ,
"name.surname@company.com" ,
];
validEmails . forEach (( email ) => {
it ( `should accept valid email: ${ email } ` , () => {
expect ( validateEmail ( email )). toBe ( true );
});
});
const invalidEmails = [
"notanemail" ,
"@example.com" ,
"user@" ,
];
invalidEmails . forEach (( email ) => {
it ( `should reject invalid email: ${ email } ` , () => {
expect ( validateEmail ( email )). toBe ( false );
});
});
});
Mocking Strategies
Database Mocks
Place mocks in __mocks__ directory:
// __mocks__/@formbricks/database.ts
export const prisma = {
survey: {
findMany: vi . fn (),
findUnique: vi . fn (),
create: vi . fn (),
update: vi . fn (),
delete: vi . fn (),
},
};
Network Mocks
import { vi } from "vitest" ;
global . fetch = vi . fn ();
vi . mocked ( fetch ). mockResolvedValue ({
ok: true ,
json : async () => ({ data: "test" }),
} as Response );
Environment Variables
import { vi } from "vitest" ;
vi . stubEnv ( "DATABASE_URL" , "postgresql://test" );
vi . stubEnv ( "NEXTAUTH_SECRET" , "test-secret" );
Best Practices
Do’s
Write tests before fixing bugs (TDD for bug fixes)
Test edge cases and error scenarios
Keep tests independent and isolated
Use descriptive test names
Mock external dependencies
Clean up after tests (afterEach, afterAll)
Don’ts
Don’t test implementation details
Don’t write flaky tests
Don’t skip tests (fix or remove them)
Don’t test external libraries
Don’t use timeouts as assertions
Don’t commit failing tests
Test Naming
Use descriptive names that explain what’s being tested:
it ( "should create survey with valid data" )
it ( "should throw error when name is missing" )
it ( "should filter surveys by status" )
Keep Tests Fast
Mock external services (don’t make real API calls)
Use in-memory databases when possible
Minimize setup/teardown
Run tests in parallel
Continuous Integration
Tests run automatically on every PR:
# GitHub Actions runs:
- pnpm lint
- pnpm test
- pnpm build
- pnpm test:e2e (on main branch)
Requirements:
All tests must pass
Coverage must not decrease
No ESLint errors
Debugging Tests
Vitest Debugging
# Run single test file
pnpm test path/to/test.test.ts
# Run tests matching pattern
pnpm test --grep "survey creation"
# Debug with VS Code
# Add breakpoint and use "Debug" in VS Code
Playwright Debugging
# Run in UI mode
px playwright test --ui
# Run in headed mode
px playwright test --headed
# Run with debugger
px playwright test --debug
# Generate test
px playwright codegen
Common Issues
Increase timeout for slow tests: it ( "slow test" , async () => {
// ...
}, 10000 ); // 10 second timeout
Ensure mock is placed before imports: vi . mock ( "module" , () => ({ ... }));
import { fn } from "module" ; // After mock
Always await async operations: await expect ( asyncFn ()). resolves . toBe ( value );
Resources
Vitest Docs Official Vitest documentation
Playwright Docs Official Playwright documentation
Testing Library Testing Library documentation
Code Style Code style guidelines