minitest について
minitestは、RailsアプリケーションのMinitestテスト作成を支援するスキルです。テストの新規作成や移行、改善に向けて、fixtureの活用や挙動を重視したアサーションを案内します。
主な機能
- 01Railsのテスト種別に沿った構成
- 02fixtureと最小限のテストデータの活用
- 03挙動を重視した安定性の高いアサーション
- 04RSpecからMinitestへの対応表
- 05マルチテナントと権限テストの手順
- 06高速化とデバッグのガイダンス
活用シーン
- 01モデル、リクエスト、ポリシーのMinitest作成
- 02RSpecテストのMinitestへの移行
- 03遅い、または不安定なテストの調査
- 04Railsプロジェクトでのテスト駆動開発
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill minitestインストール後、Claude Code で/minitestを入力して読み込まれたか確認します。
同梱ファイルreference.md
詳細
Rails Minitest エキスパート
Minitest とフィクスチャを使用して、パフォーマンスが高く、保守しやすく、壊れにくい Rails アプリケーションのテストを書きます。
基本方針
基本原則:
- 実装ではなく振る舞いをテストする - コードがどのように動くかではなく、何をするかに注目します
- 素早いフィードバックループ - インテグレーションテストよりユニットテストを、ファクトリーよりフィクスチャを優先します
- テストはドキュメント - テスト名には期待する振る舞いを記述します
- テストデータは最小限に - 必要なものだけを作成します。レコードは 2 件あれば十分なことが多いです
- 壊れにくいアサーション - 変更される可能性のある正確な値ではなく、結果をテストします
テストピラミッド:
/\ 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)
| RSpec | Minitest |
|---|---|
expect(x).to eq(y) | assert_equal y, x |
expect(x).to be_truthy | assert x |
expect(x).to be_falsey | refute x |
expect(x).to be_nil | assert_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_valid | assert x.valid? |
expect(x).not_to be_valid | refute 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パターンを使用している
避けるべきアンチパターン
- 実装の詳細をテストする — 内部のメソッド呼び出しではなく、結果をテストします
- セットアップが複雑すぎる — セットアップが 10 行を超える場合は、フィクスチャにリファクタリングします
- テスト間で状態を共有する — 各テストは独立させます
- プライベートメソッドをテストする — パブリックインターフェースのみをテストします
- 壊れやすいアサーション — タイムスタンプ、厳密なエラー内容、順序をアサートしないでください
- システムテストが多すぎる — 重要なユーザーパスにのみ使用します
- 否定テストが不足している — 失敗または拒否されるべきケースを必ずテストします
- ファクトリーの連鎖 — 関連レコードを多数作成するファクトリーは避けます
テストの実行
# 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
コメント
Markdown 対応