第6部:品質と効率化 Step 18 / 24

テスト入門

テストを書くことで、バグを早期発見し、安心してコードを変更できるようになります。

テストの種類

単体テスト

関数やコンポーネント単位

  • ・実行が速い
  • ・問題の特定が簡単
  • ・最も多く書く

結合テスト

複数の部品の連携

  • ・API + DB
  • ・コンポーネント間
  • ・中程度の量

E2Eテスト

ユーザー視点の動作

  • ・ブラウザ操作を再現
  • ・実行が遅い
  • ・重要なフローのみ

FastAPI のテスト(pytest)

セットアップ

pip install pytest pytest-asyncio httpx

tests/conftest.py

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base, get_db
from main import app

# テスト用DB
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
TestingSessionLocal = sessionmaker(bind=engine)

@pytest.fixture(scope="function")
def db():
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function")
def client(db):
    def override_get_db():
        yield db

    app.dependency_overrides[get_db] = override_get_db
    with TestClient(app) as c:
        yield c
    app.dependency_overrides.clear()

tests/test_posts.py

def test_create_post(client, db):
    # ユーザー作成
    client.post("/auth/register", json={
        "email": "test@example.com",
        "name": "Test",
        "password": "password"
    })

    # ログイン
    response = client.post("/auth/login", data={
        "username": "test@example.com",
        "password": "password"
    })
    token = response.json()["access_token"]

    # 記事作成
    response = client.post(
        "/posts",
        json={"title": "Test Post", "content": "Test Content"},
        headers={"Authorization": f"Bearer {token}"}
    )

    assert response.status_code == 201
    assert response.json()["title"] == "Test Post"


def test_get_posts(client):
    response = client.get("/posts")
    assert response.status_code == 200
    assert isinstance(response.json(), list)


def test_get_post_not_found(client):
    response = client.get("/posts/999")
    assert response.status_code == 404

Next.js のテスト(Jest + RTL)

セットアップ

npm install -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom

__tests__/Button.test.tsx

import { render, screen, fireEvent } from "@testing-library/react";
import { Button } from "@/components/ui/Button";

describe("Button", () => {
  it("renders correctly", () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText("Click me")).toBeInTheDocument();
  });

  it("calls onClick when clicked", () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click</Button>);

    fireEvent.click(screen.getByText("Click"));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it("is disabled when disabled prop is true", () => {
    render(<Button disabled>Disabled</Button>);
    expect(screen.getByText("Disabled")).toBeDisabled();
  });
});

__tests__/LoginForm.test.tsx

import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { LoginForm } from "@/components/LoginForm";

// モック
jest.mock("@/contexts/AuthContext", () => ({
  useAuth: () => ({
    login: jest.fn().mockResolvedValue(undefined),
  }),
}));

describe("LoginForm", () => {
  it("submits form with email and password", async () => {
    render(<LoginForm />);

    fireEvent.change(screen.getByLabelText("メールアドレス"), {
      target: { value: "test@example.com" },
    });

    fireEvent.change(screen.getByLabelText("パスワード"), {
      target: { value: "password" },
    });

    fireEvent.click(screen.getByText("ログイン"));

    await waitFor(() => {
      // アサーション
    });
  });

  it("shows error on invalid credentials", async () => {
    // エラーケースのテスト
  });
});

テスト実行

# FastAPI (pytest)
pytest                      # 全テスト実行
pytest tests/test_posts.py  # 特定ファイル
pytest -v                   # 詳細表示
pytest --cov=.              # カバレッジ

# Next.js (jest)
npm test                    # 全テスト実行
npm test -- --watch         # ウォッチモード
npm test -- --coverage      # カバレッジ

AIにテストを書いてもらう

プロンプト例

以下の関数のテストを書いてください。

[関数のコード]

テスト要件:
- 正常系と異常系の両方
- エッジケースも考慮
- pytestを使用

まとめ

  • 単体テストを中心に、重要なフローはE2Eも
  • FastAPIはpytest + TestClient
  • Next.jsはJest + React Testing Library
  • AIにテストコードの生成を依頼できる
エラーハンドリング 次へ:AIコードレビュー