Python: pytest
Installation & Setup
# Install dependencies
pip install pytest pytest-asyncio httpx
# pytest.ini (configuration file)
[pytest]
testpaths = tests
asyncio_mode = autoAPI Test Example
# tests/test_projects.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_project():
"""Test project creation"""
response = client.post(
"/api/v1/projects",
json={"name": "Test Project", "description": "A test"}
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Test Project"
assert "id" in data
def test_create_project_validation():
"""Test name field validation"""
response = client.post("/api/v1/projects", json={})
assert response.status_code == 422 # Validation error
def test_get_projects_empty():
"""Test empty list response"""
response = client.get("/api/v1/projects")
assert response.status_code == 200
assert response.json()["items"] == []Running Tests
# Run all tests
pytest
# Verbose output
pytest -v
# Specific file only
pytest tests/test_projects.py
# Specific function only
pytest tests/test_projects.py::test_create_project
# Check coverage
pip install pytest-cov
pytest --cov=app --cov-report=htmlFastAPI TestClient
FastAPI's TestClient allows you to
test APIs without making actual HTTP requests.
Use httpx.AsyncClient if you need async testing.
Node.js: Vitest
Installation & Setup
# Install dependencies
npm install -D vitest
# package.json scripts
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: false,
environment: 'node', // or 'jsdom' (browser environment)
},
});Hono API Test Example
// src/index.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { app } from './index';
describe('Todos API', () => {
// Initialize storage before each test
beforeEach(() => {
// Reset storage
});
describe('POST /api/v1/todos', () => {
it('should create a todo', async () => {
const res = await app.request('/api/v1/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test Todo' }),
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.title).toBe('Test Todo');
expect(data.is_completed).toBe(false);
});
it('should return 422 for empty title', async () => {
const res = await app.request('/api/v1/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '' }),
});
expect(res.status).toBe(422); // Validation error
});
});
describe('PATCH /api/v1/todos/:id', () => {
it('should toggle todo completion', async () => {
// Create a todo first
const createRes = await app.request('/api/v1/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test' }),
});
const { id } = await createRes.json();
// Toggle completion
const res = await app.request(`/api/v1/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ is_completed: true }),
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.is_completed).toBe(true);
});
});
});Hono's app.request()
Hono's app.request() method allows you to
simulate HTTP requests without running an actual server.
Vue: Vitest + @vue/test-utils
Setup
# Install dependencies
npm install -D vitest @vue/test-utils jsdom
# vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom', // Simulate browser environment
globals: false,
},
});Composable Test Example
// src/__tests__/useTodos.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { flushPromises } from '@vue/test-utils';
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query';
import { createApp, h, defineComponent } from 'vue';
import { useTodos, useCreateTodo } from '../composables/useTodos';
// Helper for testing Vue composables
function withSetup<T>(composable: () => T) {
let result!: T;
const TestComponent = defineComponent({
setup() {
result = composable();
return () => h('div');
},
});
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const app = createApp(TestComponent);
app.use(VueQueryPlugin, { queryClient });
app.mount(document.createElement('div'));
return { result, app };
}
describe('useTodos', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('should fetch todos list', async () => {
const mockResponse = {
items: [{ id: '1', title: 'Todo 1', is_completed: false }],
total: 1,
};
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockResponse),
});
const { result, app } = withSetup(() => useTodos());
await flushPromises();
await new Promise(resolve => setTimeout(resolve, 100));
expect(result.data.value).toEqual(mockResponse);
app.unmount();
});
});jsdom environment required
Vue component and composable tests require environment: 'jsdom'.
Without it, you'll get a document is not defined error.
React: Vitest + @testing-library/react
Setup
# Install dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdomHook Test Example
// src/__tests__/useProjects.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useProjects, useCreateProject } from '../hooks/useProjects';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe('useProjects', () => {
it('should fetch projects', async () => {
const mockData = { items: [{ id: '1', name: 'Test' }], total: 1 };
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockData),
});
const { result } = renderHook(() => useProjects(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.data).toEqual(mockData);
});
});
});Mocking
Mock external dependencies (APIs, databases) to write isolated tests.
Mocking fetch in Vitest
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
// Mock global fetch
const originalFetch = global.fetch;
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
global.fetch = originalFetch;
});
it('should handle API response', async () => {
// Mock success response
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
} as Response);
// Run test
const result = await fetchData();
expect(result.data).toBe('test');
});
it('should handle API error', async () => {
// Mock error response
vi.mocked(global.fetch).mockResolvedValue({
ok: false,
status: 500,
} as Response);
// Verify error handling
await expect(fetchData()).rejects.toThrow();
});Best Practices
DO
- ✅ One assertion per test
- ✅ Make intent clear in test names
- ✅ Mock external dependencies
- ✅ Test edge cases
- ✅ Test error scenarios
DON'T
- ❌ Share state between tests
- ❌ Call real APIs/DBs
- ❌ Depend on sleep/setTimeout
- ❌ Test implementation details
- ❌ Use magic numbers
Next steps
Once you've mastered unit tests, learn how to test full user scenarios with Playwright in the E2E Test Guide.