Skip to main content
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: October 7, 2026 · Version: v0.0.1

Send Feedback

Opens a new issue page with your message.

Open GitHub Issue