test-specialist について
JavaScript/TypeScript、Python、Go、Rustのテスト作成を扱うスキルです。テストを明示的に求められた場合に、単体・統合・E2E・セキュリティ・性能テストのパターンと不具合分析の手順を提供します。
主な機能
- 01JS/TS、Python、Go、Rustに対応
- 02単体・統合・E2Eテストのパターン
- 03コンポーネントテストと動作検証
- 04セキュリティ・性能テストの例
- 05体系的な不具合分析プロセス
活用シーン
- 01依頼された機能のテスト作成
- 02重要な処理のテストカバレッジ改善
- 03セキュリティまたは性能テストの作成
- 04再現と切り分けによる不具合分析
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ travisjneuman/ .claude --skill test-specialistインストール後、Claude Code で/test-specialistを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
index.jspackage.jsonreferences/bug_analysis.mdtesting_patterns.mdscripts/analyze_coverage.pyfind_untested_code.py詳細
テストスペシャリスト
JS/TS、Python、Go、Rust のテスト作成パターンと、テストを求められた際に役立つバグ分析の習慣です。
使用するタイミング
ユーザーがテストを明示的に求めた場合にのみ、このスキルを使用してください。そうでない場合は、差分と、変更が加わるコードをレビューして検証してください。
以下の場合に使用します(テストを求められた場合):
- 単体テスト、統合テスト、E2E テストの作成
- テストカバレッジの向上
- セキュリティテストとパフォーマンステスト
以下の場合は使用しないでください:
- テストの依頼なしでのデバッグ →
debug-systematicを使用 - コードレビュー →
generic-code-reviewerを使用 - 技術的負債 →
tech-debt-analyzerを使用 - 機能開発 →
generic-feature-developerを使用
プロジェクト別のテストスタック
| プロジェクトタイプ | 単体テスト | コンポーネントテスト | E2E |
|---|---|---|---|
| React/Next.js | Vitest/Jest | Testing Library | Playwright |
| Node.js | Vitest/Jest | Supertest | Playwright |
| 静的 | 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) });
});
バグ分析のプロセス
- 再現 - 正確な手順と、期待される結果と実際の結果を記録します
- 切り分け - 二分探索を行い、最小限の再現ケースを作成します
- 根本原因 - 実行の流れを追い、前提を確認し、git blame を調べます
- 修正 - 根本原因を修正します(ユーザーがテストを求めた場合に限り、先に失敗するテストを書きます)
- 検証 - 再現手順とエッジケースに照らして、差分と影響するコードパスをレビューします
デバッグチェックリスト
問題をデバッグする際は、以下を確認してください。
- 一貫して再現できる
- 最小限の再現ケースを作成した
- コンソール/ネットワークのログを確認した
- 失敗時点の状態を調べた
- 最近の変更について 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つの振る舞い
- 説明的な名前 - テスト名でシナリオを説明します
- 独立したテスト - 状態を共有しません
- エッジケースを網羅 - null、空、境界、エラー
- 外部依存をモック - テストを高速に保ちます
- 振る舞いをテスト - 実装の詳細ではありません
ワークフローの判断表
| 状況 | 対応 |
|---|---|
| 機能追加(テストの依頼あり) | 先にテストを書く(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 | クラウドでのクロスブラウザー比較 | 複数ブラウザー、チームレビュー |
| Chromatic | Storybook のストーリースナップショット | コンポーネントライブラリのビジュアル QA |
関連項目
- コードレビュー基準 - 品質要件
- プロジェクトの
CLAUDE.md- テストルール
コメント
Markdown 対応