Testing Standards
Testing Philosophy at Kirana Labs
We believe that testing is an integral part of the development process, not an afterthought. Well-tested code leads to higher quality, fewer bugs, and more maintainable applications. This document outlines our approach to testing across our technology stack.
Test Coverage Goals
We aim for the following test coverage in our projects:
| Project Type | Unit Tests | Integration Tests | E2E Tests |
|---|---|---|---|
| Client Projects | 70%+ | Critical flows | Key user journeys |
| Internal Tools | 60%+ | As needed | Optional |
Testing Pyramid
We follow the testing pyramid approach, with:
- Many unit tests (fast, focused on business logic)
- Some integration tests (testing component interactions)
- Few end-to-end tests (slow but comprehensive)
Types of Tests
Unit Tests
Unit tests are focused on testing individual functions, methods, or components in isolation.
When to write unit tests:
- For complex business logic
- For utility functions
- For critical algorithms
- For reusable components
Frameworks and Tools:
- Jest for JavaScript/TypeScript
- React Testing Library for React components
- pytest for Python/Django
Example Unit Test (TypeScript with Jest):
// utils/formatCurrency.ts
export function formatCurrency(value: number, locale = 'en-US', currency = 'USD'): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(value);
}
// utils/formatCurrency.test.ts
import { formatCurrency } from './formatCurrency';
describe('formatCurrency', () => {
it('formats USD correctly', () => {
expect(formatCurrency(1000)).toBe('$1,000.00');
});
it('formats EUR correctly', () => {
expect(formatCurrency(1000, 'de-DE', 'EUR')).toBe('1.000,00 €');
});
it('handles zero correctly', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('handles negative values correctly', () => {
expect(formatCurrency(-1000)).toBe('-$1,000.00');
});
});
Integration Tests
Integration tests verify that different parts of the application work together correctly.
When to write integration tests:
- For testing API endpoints
- For database interactions
- For component compositions
- For form submissions
Frameworks and Tools:
- Jest with supertest for API testing
- React Testing Library for testing component interactions
- Django Test Client for Django views
Example Integration Test (API Endpoint):
// __tests__/api/users.test.ts
import { createMocks } from 'node-mocks-http';
import { handler } from '@/app/api/users/route';
describe('Users API', () => {
it('returns a list of users', async () => {
const { req, res } = createMocks({
method: 'GET',
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
const data = JSON.parse(res._getData());
expect(Array.isArray(data.users)).toBe(true);
});
it('creates a new user', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Test User',
email: 'test@example.com',
role: 'user',
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(201);
const data = JSON.parse(res._getData());
expect(data.user).toHaveProperty('id');
expect(data.user.name).toBe('Test User');
});
it('validates input data', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
name: 'Test User',
// Missing email
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(400);
const data = JSON.parse(res._getData());
expect(data).toHaveProperty('error');
});
});
End-to-End (E2E) Tests
E2E tests verify that entire user flows work as expected from start to finish.
When to write E2E tests:
- For critical user journeys
- For authentication flows
- For checkout processes
- For form submissions with side effects
Frameworks and Tools:
- Playwright or Cypress for web applications
Example E2E Test (Playwright):
// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Authentication flow', () => {
test('successful login', async ({ page }) => {
// Navigate to login page
await page.goto('/login');
// Fill in credentials
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'password123');
// Click login button
await page.click('button[type="submit"]');
// Verify redirection to dashboard
await expect(page).toHaveURL(/dashboard/);
// Verify user is logged in
const userMenu = page.locator('[data-testid="user-menu"]');
await expect(userMenu).toBeVisible();
await expect(userMenu).toContainText('Test User');
});
test('unsuccessful login with wrong password', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
// Should stay on login page
await expect(page).toHaveURL(/login/);
// Should show error message
const errorMessage = page.locator('[data-testid="login-error"]');
await expect(errorMessage).toBeVisible();
await expect(errorMessage).toContainText('Invalid credentials');
});
});
Test Structure and Organization
Directory Structure
project/
├── __tests__/ # Jest tests
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── e2e/ # End-to-end tests (or in separate directory)
├── src/
│ ├── components/
│ │ ├── Button.tsx
│ │ └── Button.test.tsx # Component tests next to implementation
│ ├── utils/
│ │ ├── format.ts
│ │ └── format.test.ts # Utility tests next to implementation
│ └── ...
└── playwright/ # Playwright E2E tests in separate directory
├── tests/
│ └── login.spec.ts
└── playwright.config.ts
Test File Naming Conventions
- Unit/Integration Tests:
*.test.tsor*.spec.ts - E2E Tests:
*.spec.ts(Playwright/Cypress)
Writing Effective Tests
Test Organization (Arrange-Act-Assert)
Follow the AAA pattern for clear, maintainable tests:
- Arrange: Set up test data and conditions
- Act: Perform the action being tested
- Assert: Verify the result is as expected
test('adds items to cart', async () => {
// Arrange
const product = { id: '123', name: 'Test Product', price: 9.99 };
const cart = new ShoppingCart();
// Act
cart.addItem(product, 2);
// Assert
expect(cart.items).toHaveLength(1);
expect(cart.items[0].product).toEqual(product);
expect(cart.items[0].quantity).toBe(2);
expect(cart.total).toBe(19.98);
});
Test Naming
Use descriptive test names that explain:
- What is being tested
- Under what conditions
- What the expected result is
Format: it('should [expected behavior] when [condition]')
// Good test names
it('should format currency with correct symbols', () => {});
it('should throw error when email is invalid', () => {});
it('should add product to cart when in stock', () => {});
// Bad test names
it('test formatCurrency', () => {});
it('works correctly', () => {});
Mocking and Test Doubles
Use mocks and test doubles to isolate the code being tested:
// Example of mocking a database service
jest.mock('@/lib/db', () => ({
getUserById: jest.fn().mockImplementation((id) => {
if (id === 'valid-id') {
return Promise.resolve({
id: 'valid-id',
name: 'Test User',
email: 'test@example.com',
});
}
return Promise.resolve(null);
}),
createUser: jest.fn().mockImplementation((data) => {
return Promise.resolve({
id: 'new-id',
...data,
});
}),
}));
// In test
import { getUserById, createUser } from '@/lib/db';
test('getUserById returns user for valid ID', async () => {
const user = await getUserById('valid-id');
expect(user).toHaveProperty('name', 'Test User');
});
Testing React Components
Component Testing Principles
- Test behavior, not implementation
- Focus on user interaction, not internal state
- Use data-testid attributes for test selectors
- Test accessibility where relevant
Example Component Test
// components/LoginForm.tsx
import { useState } from 'react';
interface LoginFormProps {
onSubmit: (credentials: { email: string; password: string }) => void;
isLoading?: boolean;
}
export function LoginForm({ onSubmit, isLoading = false }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit} data-testid="login-form">
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
data-testid="email-input"
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
data-testid="password-input"
/>
</div>
<button
type="submit"
disabled={isLoading}
data-testid="login-button"
>
{isLoading ? 'Logging in...' : 'Log in'}
</button>
</form>
);
}
// components/LoginForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('submits the form with email and password', () => {
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
// Fill in the form
fireEvent.change(screen.getByTestId('email-input'), {
target: { value: 'test@example.com' },
});
fireEvent.change(screen.getByTestId('password-input'), {
target: { value: 'password123' },
});
// Submit the form
fireEvent.click(screen.getByTestId('login-button'));
// Verify submission
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
it('disables the submit button when loading', () => {
render(<LoginForm onSubmit={jest.fn()} isLoading={true} />);
const button = screen.getByTestId('login-button');
expect(button).toBeDisabled();
expect(button).toHaveTextContent('Logging in...');
});
});
Testing API Routes and Serverless Functions
// Example of testing a NextJS API route
import { createMocks } from 'node-mocks-http';
import { handler } from '@/app/api/auth/login/route';
import { authenticateUser } from '@/lib/auth';
// Mock dependencies
jest.mock('@/lib/auth', () => ({
authenticateUser: jest.fn(),
}));
describe('Login API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('returns 200 and user data when login is successful', async () => {
const mockUser = { id: '123', email: 'test@example.com', name: 'Test User' };
(authenticateUser as jest.Mock).mockResolvedValue(mockUser);
const { req, res } = createMocks({
method: 'POST',
body: {
email: 'test@example.com',
password: 'password123',
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(JSON.parse(res._getData())).toEqual({
success: true,
user: mockUser,
});
});
it('returns 401 when credentials are invalid', async () => {
(authenticateUser as jest.Mock).mockResolvedValue(null);
const { req, res } = createMocks({
method: 'POST',
body: {
email: 'test@example.com',
password: 'wrongpassword',
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(401);
expect(JSON.parse(res._getData())).toEqual({
success: false,
error: 'Invalid credentials',
});
});
});
Test-Driven Development (TDD)
For critical features or complex logic, we encourage using Test-Driven Development:
- Write a failing test that defines the expected behavior
- Write the minimal implementation to make the test pass
- Refactor the code while keeping tests passing
Benefits of TDD:
- Ensures code is testable from the start
- Helps identify design issues early
- Results in higher test coverage
- Provides documentation of the code's behavior
Testing PayloadCMS Collections
// Example of testing PayloadCMS collection hooks
import { validateUser } from '../collections/Users/hooks/validateUser';
describe('User validation hook', () => {
it('should validate email format', async () => {
const mockData = {
email: 'invalid-email',
name: 'Test User',
};
await expect(validateUser({ data: mockData })).rejects.toThrow(
'Invalid email format'
);
});
it('should transform email to lowercase', async () => {
const mockData = {
email: 'Test@Example.com',
name: 'Test User',
};
const result = await validateUser({ data: mockData });
expect(result.email).toBe('test@example.com');
});
});
Continuous Integration Testing
All tests should be integrated into our CI/CD pipeline:
- Unit and integration tests run on every PR
- E2E tests run on merges to the
devbranch and before production deployment - Test failures block merges to protect code quality
Our GitHub Actions workflow should include:
name: Test Suite
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Unit and integration tests
run: npm test
- name: Build
run: npm run build
- name: E2E tests
run: npm run test:e2e
Testing Improvement Strategy
To improve our testing practices at Kirana Labs, we should:
- Add test coverage reports to identify under-tested areas
- Implement testing standards in code reviews
- Add automated testing to the CI/CD pipeline
- Conduct testing workshops for the engineering team
- Create testing templates for common components and features
Conclusion
Effective testing is crucial for maintaining code quality and ensuring reliable applications. By following these testing standards, we can create a robust testing culture at Kirana Labs and deliver higher quality software to our clients.
Remember, the goal of testing is not to achieve arbitrary coverage numbers but to have confidence in our code and prevent regressions. Focus on testing the functionality that matters most to users and the business.