mcpField
T
test-specialist

test-specialist

github.com/travisjneumanテスト・品質106134 回インストール今日更新
リポジトリ

test-specialist について

JavaScript/TypeScript、Python、Go、Rustのテスト作成を扱うスキルです。テストを明示的に求められた場合に、単体・統合・E2E・セキュリティ・性能テストのパターンと不具合分析の手順を提供します。

主な機能

  1. 01JS/TS、Python、Go、Rustに対応
  2. 02単体・統合・E2Eテストのパターン
  3. 03コンポーネントテストと動作検証
  4. 04セキュリティ・性能テストの例
  5. 05体系的な不具合分析プロセス

活用シーン

  1. 01依頼された機能のテスト作成
  2. 02重要な処理のテストカバレッジ改善
  3. 03セキュリティまたは性能テストの作成
  4. 04再現と切り分けによる不具合分析

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/travisjneuman/.claude --skill test-specialist

インストール後、Claude Code で/test-specialistを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

index.js
package.json
references/
bug_analysis.md
testing_patterns.md
scripts/
analyze_coverage.py
find_untested_code.py

詳細

テストスペシャリスト

JS/TS、Python、Go、Rust のテスト作成パターンと、テストを求められた際に役立つバグ分析の習慣です。

使用するタイミング

ユーザーがテストを明示的に求めた場合にのみ、このスキルを使用してください。そうでない場合は、差分と、変更が加わるコードをレビューして検証してください。

以下の場合に使用します(テストを求められた場合):

  • 単体テスト、統合テスト、E2E テストの作成
  • テストカバレッジの向上
  • セキュリティテストとパフォーマンステスト

以下の場合は使用しないでください:

  • テストの依頼なしでのデバッグ → debug-systematic を使用
  • コードレビュー → generic-code-reviewer を使用
  • 技術的負債 → tech-debt-analyzer を使用
  • 機能開発 → generic-feature-developer を使用

プロジェクト別のテストスタック

プロジェクトタイプ単体テストコンポーネントテストE2E
React/Next.jsVitest/JestTesting LibraryPlaywright
Node.jsVitest/JestSupertestPlaywright
静的Jest-Playwright

ブラウザー自動化(Playwright、Percy、Chromatic、npx によるブラウザーのインストール)は、オーナーの Mac にはインストールされておらず、実行もされません。ユーザーが求めた場合に限り、ユーザーが承認した環境で実行してください。

テストパターン

単体テスト(AAA パターン)
describe("calculateTotal", () => {
  test("sums amounts correctly", () => {
    // Arrange
    const items = [{ amount: 100 }, { amount: 50 }];
    // Act
    const total = calculateTotal(items);
    // Assert
    expect(total).toBe(150);
  });

  test("handles empty list", () => {
    expect(calculateTotal([])).toBe(0);
  });
});
コンポーネントテスト(ユーザーの操作)
// ✅ Test user behavior, not implementation
it('creates item when user clicks Add', async () => {
  const user = userEvent.setup();
  render(<ItemList />);

  await user.click(screen.getByRole('button', { name: /add/i }));
  await user.type(screen.getByLabelText(/title/i), 'New item');
  await user.click(screen.getByRole('button', { name: /save/i }));

  expect(screen.getByText('New item')).toBeInTheDocument();
});
E2E テスト(Playwright)
import { test, expect } from "@playwright/test";

test("user can complete checkout", async ({ page }) => {
  await page.goto("/products");

  // Add to cart
  await page.click('button:has-text("Add to Cart")');
  await page.click('a:has-text("Cart")');

  // Checkout
  await page.click('button:has-text("Checkout")');
  await page.fill('[name="email"]', "[email protected]");
  await page.click('button:has-text("Place Order")');

  // Verify
  await expect(page.locator("h1")).toContainText("Order Confirmed");
});
統合テスト
test("POST /items creates item", async () => {
  const response = await request(app)
    .post("/api/items")
    .send({ name: "Test" })
    .expect(201);

  expect(response.body).toMatchObject({ id: expect.any(Number) });
});

バグ分析のプロセス

  1. 再現 - 正確な手順と、期待される結果と実際の結果を記録します
  2. 切り分け - 二分探索を行い、最小限の再現ケースを作成します
  3. 根本原因 - 実行の流れを追い、前提を確認し、git blame を調べます
  4. 修正 - 根本原因を修正します(ユーザーがテストを求めた場合に限り、先に失敗するテストを書きます)
  5. 検証 - 再現手順とエッジケースに照らして、差分と影響するコードパスをレビューします

デバッグチェックリスト

問題をデバッグする際は、以下を確認してください。

  • 一貫して再現できる
  • 最小限の再現ケースを作成した
  • コンソール/ネットワークのログを確認した
  • 失敗時点の状態を調べた
  • 最近の変更について git blame を確認した
  • 再現手順に照らして修正をレビューした(テストの依頼があった場合に限り、失敗するテストを使用)

よくあるバグのパターン

競合状態
test("handles concurrent updates", async () => {
  const promises = Array.from({ length: 100 }, () => increment());
  await Promise.all(promises);
  expect(getCount()).toBe(100);
});
Null 安全性
test.each([null, undefined, "", 0])("handles invalid input: %p", (input) => {
  expect(() => process(input)).toThrow("Invalid");
});
境界値
test("handles edge cases", () => {
  expect(paginate([], 1, 10)).toEqual([]); // empty
  expect(paginate([item], 1, 10)).toEqual([item]); // single
  expect(paginate(items25, 3, 10)).toHaveLength(5); // partial last page
});

セキュリティテスト

test("prevents SQL injection", async () => {
  const malicious = "'; DROP TABLE users; --";
  await expect(search(malicious)).resolves.not.toThrow();
});

test("sanitizes XSS", () => {
  const xss = '<script>alert("xss")</script>';
  expect(sanitize(xss)).not.toContain("<script>");
});

test("requires auth", async () => {
  await request(app).post("/api/items").expect(401);
});

パフォーマンステスト

test("handles large datasets efficiently", () => {
  const largeList = Array.from({ length: 10000 }, (_, i) => ({ value: i }));
  const start = performance.now();
  process(largeList);
  expect(performance.now() - start).toBeLessThan(100);
});

カバレッジの目標

コードの種類目標
重要なパス90%+
ビジネスロジック85%+
UI コンポーネント75%+
ユーティリティ70%+

テスト品質の原則

  1. 1つのテストにつき1つの振る舞い
  2. 説明的な名前 - テスト名でシナリオを説明します
  3. 独立したテスト - 状態を共有しません
  4. エッジケースを網羅 - null、空、境界、エラー
  5. 外部依存をモック - テストを高速に保ちます
  6. 振る舞いをテスト - 実装の詳細ではありません

ワークフローの判断表

状況対応
機能追加(テストの依頼あり)先にテストを書く(TDD)
バグ修正(テストの依頼あり)失敗するテストを書いてから修正する
カバレッジの向上不足箇所を見つけ、重要なパスを優先する
コードレビューエッジケースとエラー処理を確認する

Python のテスト(pytest)

フィクスチャとパラメーター化
import pytest
from myapp.services import UserService

@pytest.fixture
def user_service(db_session):
    """Provide a UserService with test database."""
    return UserService(session=db_session)

@pytest.fixture
def sample_user(user_service):
    """Create and return a sample user."""
    return user_service.create(name="Test User", email="[email protected]")

class TestUserService:
    def test_create_user(self, user_service):
        user = user_service.create(name="John", email="[email protected]")
        assert user.name == "John"
        assert user.id is not None

    def test_get_user_not_found(self, user_service):
        with pytest.raises(UserNotFoundError, match="User 999 not found"):
            user_service.get(999)

    @pytest.mark.parametrize("email,is_valid", [
        ("[email protected]", True),
        ("[email protected]", True),
        ("invalid", False),
        ("@example.com", False),
        ("", False),
    ])
    def test_validate_email(self, user_service, email: str, is_valid: bool):
        assert user_service.validate_email(email) == is_valid
モック
from unittest.mock import Mock, patch, AsyncMock

def test_send_notification(user_service):
    with patch("myapp.services.email_client") as mock_email:
        mock_email.send = Mock(return_value=True)
        user_service.notify(user_id=1, message="Hello")
        mock_email.send.assert_called_once_with(
            to="[email protected]",
            body="Hello",
        )

# Async mocking
@pytest.mark.asyncio
async def test_fetch_data():
    with patch("myapp.client.fetch", new_callable=AsyncMock) as mock_fetch:
        mock_fetch.return_value = {"status": "ok"}
        result = await process_data()
        assert result["status"] == "ok"
conftest.py のパターン
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture(scope="session")
def engine():
    return create_engine("sqlite:///:memory:")

@pytest.fixture(scope="function")
def db_session(engine):
    Base.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.rollback()
    session.close()
    Base.metadata.drop_all(engine)

Go のテスト

テーブル駆動テスト
func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name     string
        amount   float64
        code     string
        want     float64
        wantErr  bool
    }{
        {
            name:   "valid 10% discount",
            amount: 100.0,
            code:   "SAVE10",
            want:   90.0,
        },
        {
            name:   "no discount",
            amount: 100.0,
            code:   "",
            want:   100.0,
        },
        {
            name:    "invalid code",
            amount:  100.0,
            code:    "INVALID",
            wantErr: true,
        },
        {
            name:   "zero amount",
            amount: 0,
            code:   "SAVE10",
            want:   0,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CalculateDiscount(tt.amount, tt.code)
            if (err != nil) != tt.wantErr {
                t.Errorf("CalculateDiscount() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
            }
        })
    }
}
Testify のアサーションとモック
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/require"
)

// Mock
type MockUserRepo struct {
    mock.Mock
}

func (m *MockUserRepo) FindByID(id string) (*User, error) {
    args := m.Called(id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}

func TestGetUser(t *testing.T) {
    repo := new(MockUserRepo)
    repo.On("FindByID", "123").Return(&User{ID: "123", Name: "John"}, nil)

    service := NewUserService(repo)
    user, err := service.GetUser("123")

    require.NoError(t, err)
    assert.Equal(t, "John", user.Name)
    repo.AssertExpectations(t)
}

// HTTP handler testing
func TestGetUserHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()

    handler := NewHandler(mockService)
    handler.GetUser(w, req)

    assert.Equal(t, http.StatusOK, w.Code)

    var user User
    err := json.NewDecoder(w.Body).Decode(&user)
    require.NoError(t, err)
    assert.Equal(t, "123", user.ID)
}

Rust のテスト

単体テストと統合テスト
// src/lib.rs - Unit tests (same file)
pub fn calculate_discount(amount: f64, percentage: f64) -> Result<f64, DiscountError> {
    if percentage < 0.0 || percentage > 100.0 {
        return Err(DiscountError::InvalidPercentage(percentage));
    }
    Ok(amount * (1.0 - percentage / 100.0))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_valid_discount() {
        let result = calculate_discount(100.0, 10.0).unwrap();
        assert!((result - 90.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_zero_discount() {
        assert_eq!(calculate_discount(100.0, 0.0).unwrap(), 100.0);
    }

    #[test]
    fn test_invalid_percentage() {
        assert!(matches!(
            calculate_discount(100.0, 150.0),
            Err(DiscountError::InvalidPercentage(_))
        ));
    }

    #[test]
    fn test_negative_percentage() {
        assert!(calculate_discount(100.0, -10.0).is_err());
    }
}

// tests/integration_test.rs - Integration tests (separate file)
use my_crate::calculate_discount;

#[test]
fn test_full_workflow() {
    let original = 200.0;
    let discounted = calculate_discount(original, 25.0).unwrap();
    assert_eq!(discounted, 150.0);
}
プロパティベーステスト(proptest)
use proptest::prelude::*;

proptest! {
    #[test]
    fn discount_never_exceeds_original(amount in 0.0f64..10000.0, pct in 0.0f64..100.0) {
        let result = calculate_discount(amount, pct).unwrap();
        prop_assert!(result <= amount);
        prop_assert!(result >= 0.0);
    }

    #[test]
    fn roundtrip_serialization(name in "[a-zA-Z]{1,50}", age in 0u32..150) {
        let user = User { name: name.clone(), age };
        let json = serde_json::to_string(&user).unwrap();
        let deserialized: User = serde_json::from_str(&json).unwrap();
        prop_assert_eq!(user, deserialized);
    }
}

ビジュアルリグレッションテスト

Playwright のスクリーンショット
import { test, expect } from "@playwright/test";

test("homepage visual regression", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("homepage.png", {
    maxDiffPixelRatio: 0.01,
  });
});

test("component states", async ({ page }) => {
  await page.goto("/components");

  // Default state
  await expect(page.locator(".card")).toHaveScreenshot("card-default.png");

  // Hover state
  await page.locator(".card").hover();
  await expect(page.locator(".card")).toHaveScreenshot("card-hover.png");

  // Full page with specific viewport
  await page.setViewportSize({ width: 375, height: 812 }); // iPhone
  await expect(page).toHaveScreenshot("homepage-mobile.png");
});

// Update snapshots: npx playwright test --update-snapshots
Percy(クラウドでのビジュアルテスト)
import percySnapshot from "@percy/playwright";

test("visual test with Percy", async ({ page }) => {
  await page.goto("/dashboard");
  await percySnapshot(page, "Dashboard");
  // Percy compares across browsers and viewport sizes
});
Chromatic(Storybook のビジュアルテスト)
# Run Chromatic on Storybook stories
npx chromatic --project-token=<token>

# CI integration
# Chromatic captures every story as a visual snapshot
# and detects pixel-level changes across PRs
ツールアプローチ最適な用途
Playwrightローカルでのスクリーンショット比較E2E ビジュアルテスト、無料
Percyクラウドでのクロスブラウザー比較複数ブラウザー、チームレビュー
ChromaticStorybook のストーリースナップショットコンポーネントライブラリのビジュアル QA

関連項目

コメント