Choorai
Unit pytest Vitest

Unit Test Guide

Test individual functions, API endpoints, and components in isolation. The advantages are fast execution and easy debugging.

Principles of unit testing

  • Fast: Runs in milliseconds
  • Isolated: No impact on other tests
  • Repeatable: Same results every time
  • Self-validating: Clear pass/fail outcome

Python: pytest

Installation & Setup

Terminal
# Install dependencies
pip install pytest pytest-asyncio httpx

# pytest.ini (configuration file)
[pytest]
testpaths = tests
asyncio_mode = auto

API Test Example

tests/test_projects.py
# 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

Terminal
# 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=html

FastAPI 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

Terminal
# Install dependencies
npm install -D vitest

# package.json scripts
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}
vitest.config.ts
// 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
// 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

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
// 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

Terminal
# Install dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom

Hook Test Example

src/__tests__/useProjects.test.ts
// 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

Mocking Example
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.

Last updated: February 22, 2026 · Version: v0.0.1

Send Feedback

Opens a new issue page with your message.

Open GitHub Issue