mcpField
M
minitest

minitest

github.com/thinkoodleテスト・品質7211 回インストール8 週間前に更新
リポジトリ

minitest について

minitestは、RailsアプリケーションのMinitestテスト作成を支援するスキルです。テストの新規作成や移行、改善に向けて、fixtureの活用や挙動を重視したアサーションを案内します。

主な機能

  1. 01Railsのテスト種別に沿った構成
  2. 02fixtureと最小限のテストデータの活用
  3. 03挙動を重視した安定性の高いアサーション
  4. 04RSpecからMinitestへの対応表
  5. 05マルチテナントと権限テストの手順
  6. 06高速化とデバッグのガイダンス

活用シーン

  1. 01モデル、リクエスト、ポリシーのMinitest作成
  2. 02RSpecテストのMinitestへの移行
  3. 03遅い、または不安定なテストの調査
  4. 04Railsプロジェクトでのテスト駆動開発

インストール

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

$npx skills add https://github.com/thinkoodle/rails-skills --skill minitest

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

同梱ファイルreference.md

詳細

Rails Minitest エキスパート

Minitest とフィクスチャを使用して、パフォーマンスが高く、保守しやすく、壊れにくい Rails アプリケーションのテストを書きます。

基本方針

基本原則:

  1. 実装ではなく振る舞いをテストする - コードがどのように動くかではなく、何をするかに注目します
  2. 素早いフィードバックループ - インテグレーションテストよりユニットテストを、ファクトリーよりフィクスチャを優先します
  3. テストはドキュメント - テスト名には期待する振る舞いを記述します
  4. テストデータは最小限に - 必要なものだけを作成します。レコードは 2 件あれば十分なことが多いです
  5. 壊れにくいアサーション - 変更される可能性のある正確な値ではなく、結果をテストします

テストピラミッド:

    /\     System Tests (Few - critical paths only)
   /  \    
  /____\   Request/Integration Tests (Some)
 /      \  
/________\ Unit Tests (Many - models, policies, services)

このスキルを使う場面

  • Rails のモデル、ポリシー、コントローラー、リクエストに対する新しい Minitest テストを書く場合
  • RSpec のテストを Minitest に変換する場合
  • 遅いテストや不安定なテストをデバッグする場合
  • テストスイートのパフォーマンスを改善する場合
  • Rails のテスト規約に従う場合
  • フィクスチャベースのテストデータを書く場合
  • TDD ワークフローを実装する場合

手順

ステップ 1:テストの種類を特定する

テストを書く前に、適切なテストの種類を判断します。

テストの種類場所用途
モデルtest/models/バリデーション、アソシエーション、ビジネスロジックのメソッド
ポリシーtest/policies/Pundit の認可ポリシー
リクエストtest/requests/HTTP リクエスト/レスポンスの全サイクル
コントローラーtest/controllers/コントローラーのアクション(リクエストテストを推奨)
システムtest/system/実際のブラウザーを使った重要なユーザーフロー
サービスtest/services/サービスオブジェクトと複雑な処理
ジョブtest/jobs/バックグラウンドジョブの振る舞い
メーラーtest/mailers/メールの内容と配信
ステップ 2:既存のパターンを確認する

必ず最初に既存のテストを検索してください。

# Find similar test files
rg "class.*Test < " test/

# Find existing fixtures
ls test/fixtures/

# Check for test helpers
cat test/test_helper.rb
cat test/support/*.rb

既存のプロジェクト規約に合わせてください。「最良」のパターンより一貫性が重要です。

ステップ 3:ファクトリーではなくフィクスチャを使う

フィクスチャは Factory Bot より 10〜100 倍高速です。

# AVOID - Factory Bot (slow, implicit)
let(:user) { create(:user) }
let(:project) { create(:project, workspace: workspace) }

# PREFER - Fixtures (fast, explicit)
setup do
  @workspace = workspaces(:main_workspace)
  @user = users(:admin_user)
  @project = projects(:active_project)
end

フィクスチャのベストプラクティス:

  • 目的に応じたフィクスチャを、内容がわかる名前で作成します
  • 動的な値や UUID には <%= %> を使用します
  • アソシエーションは ID ではなくフィクスチャ名で参照します
  • フィクスチャは最小限にし、必須の属性だけを含めます
# test/fixtures/users.yml
admin_user:
  id: <%= Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "admin_user") %>
  email: "[email protected]"
  name: "Admin User"
  created_at: <%= 1.week.ago %>

member_user:
  id: <%= Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "member_user") %>
  email: "[email protected]"
  name: "Member User"
  workspace: main_workspace  # Reference by fixture name
ステップ 4:テストの構成を書く

標準的なテスト構成:

require "test_helper"

class ModelTest < ActiveSupport::TestCase
  setup do
    # Load fixtures - MINIMAL setup only
    @record = models(:fixture_name)
  end

  # Group related tests with comments or test naming
  
  # Validation tests
  test "requires name" do
    @record.name = nil
    refute @record.valid?
    assert_includes @record.errors[:name], "can't be blank"
  end

  # Method tests
  test "#full_name returns formatted name" do
    @record.first_name = "John"
    @record.last_name = "Doe"
    assert_equal "John Doe", @record.full_name
  end
end
ステップ 5:パフォーマンスのガイドラインに従う

可能な限りデータベースを避ける:

# SLOW - Creates database records
test "validates email format" do
  user = User.create!(email: "invalid", name: "Test")
  refute user.valid?
end

# FAST - Uses in-memory object
test "validates email format" do
  user = User.new(email: "invalid", name: "Test")
  refute user.valid?
end

作成するレコードを最小限にする:

# SLOW - Creates 25 records
test "paginates results" do
  create_list(:post, 25)
  # ...
end

# FAST - Configure pagination threshold for tests
# config/environments/test.rb: Pagy::DEFAULT[:limit] = 2
test "paginates results" do
  # Only need 3 records to test pagination with limit of 2
  assert_operator posts.count, :>=, 3
  # ...
end

可能な限りブラウザーテストを避ける:

# SLOW - Full browser simulation
class PostsSystemTest < ApplicationSystemTestCase
  test "creates a post" do
    visit new_post_path
    fill_in "Title", with: "Test"
    click_on "Create"
    assert_text "Post created"
  end
end

# FAST - Request test (no browser)
class PostsRequestTest < ActionDispatch::IntegrationTest
  test "creates a post" do
    post posts_path, params: { post: { title: "Test" } }
    assert_response :redirect
    follow_redirect!
    assert_response :success
  end
end
ステップ 6:壊れにくいアサーションを書く

正確な値ではなく振る舞いをテストする:

# BRITTLE - Exact timestamp match
assert_equal "2025-01-15T10:00:00Z", response["created_at"]

# ROBUST - Just verify presence
assert response["created_at"].present?

# BRITTLE - Exact error message
assert_equal "Name can't be blank", record.errors.full_messages.first

# ROBUST - Check for key content
assert_includes record.errors[:name], "can't be blank"

包含アサーションを使う:

# BRITTLE - Exact match
assert_equal({ id: 1, name: "Test", email: "[email protected]" }, response)

# ROBUST - Check key attributes only
assert_equal 1, response[:id]
assert_equal "Test", response[:name]
# OR
assert response.slice(:id, :name) == { id: 1, name: "Test" }
ステップ 7:マルチテナンシーに対応する

acts_as_tenant を使うプロジェクトでは、必ずテナントのコンテキストで実行します:

# WRONG - Missing tenant context
test "admin can view project" do
  assert policy(@admin, @project).show?
end

# CORRECT - Proper tenant scoping
test "admin can view project" do
  with_workspace(@workspace) do
    assert policy(@admin, @project).show?
  end
end
ステップ 8:権限フローを正しくテストする

必ず許可する前に拒否をテストし、その後に許可をテストします:

test "member requires permission to create" do
  with_workspace(@workspace) do
    # 1. Test denial WITHOUT permission
    refute policy(@member, Project).create?
    
    # 2. Grant permission
    set_workspace_permissions(@member, @workspace, :allowed_to_create_projects)
    
    # 3. Test allow WITH permission
    assert policy(@member, Project).create?
  end
end

クイックリファレンス

アサーションの対応表(RSpec から Minitest)
RSpecMinitest
expect(x).to eq(y)assert_equal y, x
expect(x).to be_truthyassert x
expect(x).to be_falseyrefute x
expect(x).to be_nilassert_nil x
expect(arr).to include(x)assert_includes arr, x
expect(arr).not_to include(x)refute_includes arr, x
expect { }.to change { X.count }.by(1)assert_difference "X.count", 1 do ... end
expect { }.to raise_error(E)assert_raises(E) { ... }
expect(x).to be_validassert x.valid?
expect(x).not_to be_validrefute x.valid?
expect(x).to match(/pattern/)assert_match /pattern/, x
Rails 固有のアサーション
# Record changes
assert_difference "Post.count", 1 do
  Post.create!(title: "Test")
end

assert_no_difference "Post.count" do
  Post.new.save  # Invalid, doesn't save
end

# Value changes
assert_changes -> { post.reload.title }, from: "Old", to: "New" do
  post.update!(title: "New")
end

# Response assertions
assert_response :success
assert_response :redirect
assert_redirected_to post_path(post)

# DOM assertions
assert_select "h1", "Expected Title"
assert_select ".post", count: 3

# Query assertions
assert_queries_count(2) { User.find(1); User.find(2) }
assert_no_queries { cached_value }
テストファイルのテンプレート

モデルのテスト:

require "test_helper"

class UserTest < ActiveSupport::TestCase
  setup do
    @user = users(:active_user)
  end

  test "valid fixture" do
    assert @user.valid?
  end

  test "requires email" do
    @user.email = nil
    refute @user.valid?
    assert_includes @user.errors[:email], "can't be blank"
  end

  test "#display_name returns formatted name" do
    @user.name = "John Doe"
    assert_equal "John Doe", @user.display_name
  end
end

リクエストテスト:

require "test_helper"

class PostsRequestTest < ActionDispatch::IntegrationTest
  setup do
    @user = users(:active_user)
    @post = posts(:published_post)
    sign_in @user
  end

  test "GET /posts returns success" do
    get posts_path
    assert_response :success
  end

  test "POST /posts creates record" do
    assert_difference "Post.count", 1 do
      post posts_path, params: { post: { title: "New Post", body: "Content" } }
    end
    assert_redirected_to post_path(Post.last)
  end

  test "POST /posts with invalid data returns error" do
    assert_no_difference "Post.count" do
      post posts_path, params: { post: { title: "" } }
    end
    assert_response :unprocessable_entity
  end
end

ポリシーテスト:

require "test_helper"

class PostPolicyTest < ActiveSupport::TestCase
  include PolicyTestHelpers

  setup do
    @workspace = workspaces(:main_workspace)
    @admin = users(:admin_user)
    @member = users(:member_user)
    @post = posts(:workspace_post)
    Current.user = nil
  end

  test "admin can always edit" do
    with_workspace(@workspace) do
      assert policy(@admin, @post).edit?
    end
  end

  test "member requires permission to edit" do
    with_workspace(@workspace) do
      refute policy(@member, @post).edit?
      
      set_workspace_permissions(@member, @workspace, :allowed_to_edit_posts)
      assert policy(@member, @post).edit?
    end
  end

  test "scope excludes other workspace posts" do
    with_workspace(@other_workspace) do
      scope = PostPolicy::Scope.new(@admin, Post.all).resolve
      refute_includes scope, @post
    end
  end
end

パフォーマンス最適化チェックリスト

テストを提出する前に、次の項目を確認してください。

  • ファクトリーではなくフィクスチャを使用している
  • DB が不要な場合は User.create ではなく User.new を使用している
  • メモリ上のオブジェクトでバリデーションエラーをテストしている
  • フィクスチャのデータを最小限にしている(必要なものだけ)
  • 可能な場合はシステムテストではなくリクエストテストを使用している
  • テスト用にページネーションのしきい値を低く設定している
  • フィクスチャに不要な関連付けがない
  • テスト環境で BCrypt のコストを最小に設定している
  • テスト環境でログ出力を無効にしている
  • 該当する場合は build_stubbed パターンを使用している

避けるべきアンチパターン

  1. 実装の詳細をテストする — 内部のメソッド呼び出しではなく、結果をテストします
  2. セットアップが複雑すぎる — セットアップが 10 行を超える場合は、フィクスチャにリファクタリングします
  3. テスト間で状態を共有する — 各テストは独立させます
  4. プライベートメソッドをテストする — パブリックインターフェースのみをテストします
  5. 壊れやすいアサーション — タイムスタンプ、厳密なエラー内容、順序をアサートしないでください
  6. システムテストが多すぎる — 重要なユーザーパスにのみ使用します
  7. 否定テストが不足している — 失敗または拒否されるべきケースを必ずテストします
  8. ファクトリーの連鎖 — 関連レコードを多数作成するファクトリーは避けます

テストの実行

# Run all tests
bin/rails test

# Run specific file
bin/rails test test/models/user_test.rb

# Run specific test by line
bin/rails test test/models/user_test.rb:42

# Run specific test by name
bin/rails test -n "test_requires_email"

# Run directory
bin/rails test test/policies/

# Run with verbose output
bin/rails test -v

# Run in parallel
bin/rails test --parallel

# Run with coverage
COVERAGE=true bin/rails test

# Run and fail fast
bin/rails test --fail-fast

デバッグのヒント

# Print response body in request tests
puts response.body

# Print validation errors
pp @record.errors.full_messages

# Use breakpoint (Rails 7+)
debugger

# Check SQL queries
ActiveRecord::Base.logger = Logger.new(STDOUT)

# Inspect fixture data
pp users(:admin_user).attributes

コメント