iOSアプリ開発において、テストは「書けたら理想だが後回しになりがちな作業」という位置づけになっていないでしょうか。スピードを優先するあまり、テストなしでリリースを重ねた結果、リグレッションに悩まされた経験をお持ちの方も多いかと思います。
App Store のレビューポリシーが厳格化される中、クラッシュ率や品質スコアがストア評価に影響し始めています。個人開発者にとっても「1人でも品質を担保できる仕組み」が、長期的なストア評価と収益安定の鍵になっています。
Claude Code を挟むと、テスト設計・テストコード生成・CI 環境構築のいずれもが目に見えて軽くなります。以下では、私が落ち着いた構成 — XCTest・Swift Testing・XCUITest がそれぞれ得意な領域だけを担当する形 — を、実際に使ったプロンプトとともにお伝えします。
対象読者はiOSアプリ開発の経験があり、「そろそろテストをちゃんとやりたい」と思っているエンジニアです。Swiftの基本文法はご存知の前提で進めます。
iOS テストの全体像:3層テスト戦略
まず、テスト全体の戦略を整理しておきましょう。iOSアプリに適したテスト構成は「3層ピラミッド」モデルです。
最下層のユニットテスト は、個々のクラス・関数の動作を独立して検証します。実行速度が速く、フィードバックが即座に得られるため、最も数が多くなります。Swift Testing フレームワークが活躍する領域です。
中間層の統合テスト は、複数コンポーネントの連携を検証します。Repository と ViewModel の間、ViewModel と Service の間など、実際のデータフローをテストします。モックを使いすぎず、実際の依存関係を一部使用するのが特徴です。
上位層の**UIテスト(End-to-End)**は、ユーザーの実際の操作フローをシミュレートします。実行時間が長く、メンテナンスコストが高いため、最も重要な「Critical Path」のみに絞ることが鉄則です。
Claude Codeはこの3層すべてにおいて、コード生成・分析・修正のサポートを提供できます。
Claude Code × iOS テスト:なぜ相性が良いのか
Claude Codeは単なるコード補完ツールではなく、プロジェクト全体のコンテキストを理解した上でテストを生成できます。通常のAI補完との違いは次の点にあります。
CLAUDE.md によるプロジェクト文脈の共有 という仕組みが特に強力です。アーキテクチャ設計・命名規則・依存関係をClaude Codeに事前理解させることで、プロジェクト固有の慣習に沿ったテストが生成されます。一度適切な CLAUDE.md を書いてしまえば、毎回「モックはプロトコルベースで」「命名は〜Tests.swift」と伝える必要がなくなります。
既存コードの分析 においては、実装コードを読み込み、テストすべき境界値・エッジケースを自動で洗い出します。例えば「この関数のすべてのパスをテストして」と依頼すると、正常系・異常系・境界値を網羅したテストケースを提案してくれます。
テスト失敗の自己修正 については、CIパイプラインと連携させることで、テスト失敗時に自動でバグを特定・修正するループを構築できます。Hooks 機能を使ったこの自動修正ループについては後半で詳しく説明します。
CLAUDE.md の設計:iOS テストプロジェクト向け
Claude Codeの力を最大限に引き出すには、プロジェクトルートの CLAUDE.md に適切な情報を記載する点が肝心です。
# MyApp CLAUDE.md
## アーキテクチャ
- パターン: MVVM + Coordinator
- 非同期処理: Swift Concurrency (async/await + Actor)
- DI: 手製のコンテナ(ServiceLocator)を使用
## テスト方針
- Unit Test: Swift Testing フレームワーク優先(Xcode 16+)
- Integration Test: XCTest で Repository〜ViewModel 間を検証
- UI Test: XCUITest で Critical Path のみ(ログイン・購入フロー)
## テストファイル命名規則
- Unit: `{対象クラス名}Tests.swift`
- Integration: `{機能名}IntegrationTests.swift`
- UI: `{フロー名}UITests.swift`
## モック方針
- プロトコルベースのモックを使用(Mockolo 不使用)
- テストダブルは `Mocks/` フォルダに集約
## 禁止事項
- 本番コードに #if DEBUG ブロックを使わない
- sleep() による非同期待機は禁止(XCTestExpectation or async/await を使う)
この CLAUDE.md があることで、Claude Codeは「このプロジェクトでは Swift Testing を使う」「モックはプロトコルベースで書く」という制約を理解した上でテストを生成します。
Swift Testing フレームワーク:XCTest との違いと移行戦略
Swift Testing(import Testing)は Xcode 16 から正式採用されたモダンなテストフレームワークです。XCTest と比べて次の優位点があります。
@Test マクロ : func test〜() の命名規則が不要になり、テスト名を自由に付けられる
#expect マクロ : 失敗時のメッセージが詳細で、デバッグがしやすい
Parameterized Testing : 同一テストを複数の入力値で実行できる
// Swift Testing での ViewModel テスト例
import Testing
@testable import MyApp
@Suite ( "ArticleViewModel Tests" )
struct ArticleViewModelTests {
// 依存関係のセットアップ
let mockRepository: MockArticleRepository
let viewModel: ArticleViewModel
init () {
mockRepository = MockArticleRepository ()
viewModel = ArticleViewModel ( repository : mockRepository)
}
@Test ( "初回ロード時に記事一覧を取得できる" )
func fetchArticlesOnLoad () async throws {
// Arrange
let expected = [Article. stub ( id : "1" ), Article. stub ( id : "2" )]
mockRepository.stubbedArticles = expected
// Act
await viewModel. loadArticles ()
// Assert
#expect (viewModel.articles. count == 2 )
#expect (viewModel.articles. first ? .id == "1" )
#expect ( ! viewModel.isLoading)
}
@Test ( "ネットワークエラー時にエラーメッセージを表示する" )
func showErrorOnNetworkFailure () async throws {
// Arrange
mockRepository.shouldThrowError = true
// Act
await viewModel. loadArticles ()
// Assert
#expect (viewModel.errorMessage != nil )
#expect (viewModel.articles. isEmpty )
}
// Parameterized Test: 複数の入力値で同一ロジックをテスト
@Test ( "各カテゴリのフィルタリングが正しく動作する" , arguments : [
( "tech" , 3 ),
( "design" , 2 ),
( "business" , 0 )
])
func filterByCategory ( category : String , expectedCount : Int ) async throws {
mockRepository.stubbedArticles = Article. stubCollection ()
await viewModel. loadArticles ()
viewModel. filterByCategory (category)
#expect (viewModel.filteredArticles. count == expectedCount)
}
}
Claude Code でのテスト生成プロンプト
# 実装ファイルを指定してテストを生成
claude "Sources/Features/Article/ArticleViewModel.swift を読んで、
Swift Testingフレームワーク(import Testing)を使ったユニットテストを
ArticleViewModelTests.swift として生成してください。
- 全publicメソッドのハッピーパスとエラーケースをカバー
- パラメータ化テストを活用
- モックは Sources/Tests/Mocks/ の既存パターンに合わせる"
XCTest との共存:Legacy コードへの対応
既存プロジェクトに XCTest のテストが大量にある場合、一度にすべてを Swift Testing に移行するのは非現実的ですし、その必要もありません。Apple 自身も「移行は任意で、XCTest は引き続きサポートされる」と明言しています。大切なのは、新規テストは Swift Testing で書きつつ、既存の XCTest は動作している限り維持するという「共存戦略」です。
移行を検討すべきタイミングは次のようなケースです。まず、テストが頻繁に失敗するファイル(flaky tests が多いファイル)は、Swift Testing のより明示的な非同期処理ハンドリングに移行することで安定性が増します。次に、Parameterized Testing を使いたいケースです。同一ロジックを複数の入力値でテストしたい場面は XCTest では煩雑な実装が必要でしたが、Swift Testing では1行で解決します。そして、テストの可読性を高めたいファイルも移行候補です。func testUserCanLoginWithValidCredentials() より @Test("有効な認証情報でログインできる") の方が意図が明確です。
Claude Codeを使った段階的移行戦略を紹介します。
# 移行候補の特定
claude "Tests/ フォルダ配下の全 XCTest ファイルを分析して、
Swift Testing に移行しやすい順にリストアップしてください。
判断基準: setUp/tearDown の複雑さ・非同期処理の有無・依存関係の数"
Claude Codeはリスト化と優先度付けを行い、シンプルなファイルから段階的に移行することを提案します。移行時のリファクタリングも一括で依頼できます。
# 特定ファイルの移行
claude "Tests/Unit/UserServiceTests.swift を XCTest から Swift Testing に移行してください。
変換ルール:
- XCTAssertEqual → #expect(a == b)
- XCTAssertNil → #expect(value == nil)
- setUp() → init()
- tearDown() → deinit または withKnownIssue
- 非同期: XCTestExpectation → async/await
元のファイルはバックアップし、変換後のファイルで上書きしてください"
XCUITest:Critical Path の UI テスト設計
UI テストはコストが高いため、Critical Path(ユーザーが最も重要な操作)に絞って 実装するのがベストプラクティスです。
「全画面にUIテストを書く」アプローチはよく見られますが、維持コストが膨大になりすぐに破綻します。UIテストは遅い(1テストあたり数秒〜十数秒)、画面デザインが変わると壊れる、CI上で環境依存の失敗が起きやすい、という3つの弱点を持っています。これらを理解した上で「ここだけは絶対に自動テストで守りたい」というフローにだけ投資することが、長期的に維持可能なUIテスト戦略です。
Critical Pathとして選ぶべきは、ビジネス上のインパクトが最大のフロー(課金・ログイン・オンボーディングなど)と、一度壊れると修正が分かりにくいフロー(多ステップのウィザード型UIなど)です。
Claude Codeには次のように依頼します。
claude "このアプリのCritical Pathとして以下のフローをUIテスト化してください:
1. メールアドレス+パスワードでのログインフロー
2. 商品詳細→カート追加→購入完了フロー
各テストでアクセシビリティIDを使い、UI変更に強いセレクターにすること"
生成されたテストの例:
// XCUITest の例: ログインフロー
import XCTest
final class LoginFlowUITests : XCTestCase {
private var app: XCUIApplication !
override func setUpWithError () throws {
continueAfterFailure = false
app = XCUIApplication ()
// テスト用の環境変数でモックサーバーを起動
app.launchEnvironment[ "USE_MOCK_SERVER" ] = "true"
app.launchEnvironment[ "RESET_STATE" ] = "true"
app. launch ()
}
func testLoginWithValidCredentials () throws {
// ログイン画面に遷移
let loginButton = app.buttons[ "nav_login_button" ]
XCTAssertTrue (loginButton. waitForExistence ( timeout : 5 ))
loginButton. tap ()
// メールアドレス入力
let emailField = app.textFields[ "login_email_field" ]
XCTAssertTrue (emailField. waitForExistence ( timeout : 3 ))
emailField. tap ()
emailField. typeText ( "test@example.com" )
// パスワード入力
let passwordField = app.secureTextFields[ "login_password_field" ]
passwordField. tap ()
passwordField. typeText ( "TestPassword123!" )
// ログインボタンをタップ
app.buttons[ "login_submit_button" ]. tap ()
// ホーム画面への遷移を確認
let homeTitle = app.navigationBars[ "home_nav_bar" ]
XCTAssertTrue (homeTitle. waitForExistence ( timeout : 10 ),
"ログイン後にホーム画面が表示されるべき" )
}
func testLoginWithInvalidPassword () throws {
let loginButton = app.buttons[ "nav_login_button" ]
loginButton. tap ()
app.textFields[ "login_email_field" ]. tap ()
app.textFields[ "login_email_field" ]. typeText ( "test@example.com" )
app.secureTextFields[ "login_password_field" ]. tap ()
app.secureTextFields[ "login_password_field" ]. typeText ( "wrongpassword" )
app.buttons[ "login_submit_button" ]. tap ()
// エラーメッセージの表示を確認
let errorMessage = app.staticTexts[ "login_error_message" ]
XCTAssertTrue (errorMessage. waitForExistence ( timeout : 5 ))
}
}
アクセシビリティ ID の自動付与
UI テストの安定性はアクセシビリティ ID の付与に大きく依存します。Claude Codeを使って既存の SwiftUI ビューにまとめて ID を追加できます。
claude "Sources/Features/Login/ フォルダ配下の SwiftUI ビューに、
UIテスト用のアクセシビリティIDを追加してください。
命名規則: {画面名}_{要素タイプ}_{連番} 例: login_email_field, login_submit_button
変更後のファイルを上書きして、追加した ID の一覧をリストアップしてください"
テストカバレッジの計測と改善
テストカバレッジは「テストの量を測る指標」として便利ですが、万能ではありません。カバレッジが高くても、テストの質が低ければ価値は限定的です。一方で、カバレッジが低いことは確実に「テストされていないコードパスが存在する」ことを示しており、リグレッションリスクの指標として有用です。
実運用での推奨値は、ビジネスロジック(ViewModel・Service・Repository)で80%以上、全体で60〜70%以上を目安にするのが現実的です。100%を義務化すると、テストの価値がなく意味のないカバレッジ稼ぎ(trivial getterのテストなど)に工数が取られる本末転倒が起きます。
Claude Codeを使ったカバレッジ分析の特長は、「どのコードがカバーされていないか」を機械的に列挙するだけでなく、「なぜそのコードがテストしにくいか」「リファクタリングでテストしやすくできないか」まで提案してくれる点です。カバレッジ向上を単なる数字の改善としてではなく、設計改善のきっかけとして捉えることができます。
カバレッジレポートの取得
# ターミナルからカバレッジ付きでテストを実行
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-enableCodeCoverage YES \
-resultBundlePath ./TestResults.xcresult
# カバレッジレポートを JSON で出力
xcrun xccov view --report --json TestResults.xcresult > coverage.json
Claude Code でのカバレッジ分析
claude "coverage.json を分析して、
カバレッジが60%未満のファイルをリストアップし、
各ファイルについてカバーされていないコードパスのテストケースを提案してください。
優先順位はビジネスロジックを含むファイル(ViewModel, Service, Repository)を高く設定してください"
差分カバレッジチェック(PRマージ前の品質ゲート)
#!/bin/bash
# scripts/check_coverage.sh
# PR マージ前にカバレッジが閾値を下回っていないかチェック
THRESHOLD = 80
COVERAGE = $( xcrun xccov view --report --json TestResults.xcresult | \
python3 -c "import json,sys; d=json.load(sys.stdin); \
print(round(d['lineCoverage'] * 100, 1))" )
echo "Current coverage: ${ COVERAGE }%"
if (( $(echo " $COVERAGE < $THRESHOLD " | bc - l) )); then
echo "❌ Coverage ${ COVERAGE }% is below threshold ${ THRESHOLD }%"
exit 1
fi
echo "✅ Coverage check passed"
Xcode Cloud + Claude Code:完全自動 CI パイプライン
CIパイプラインの整備は、テスト基盤構築の仕上げに当たる重要な工程です。どれほど優れたテストを書いても、ローカルでしか実行されなければ効果は半減します。プルリクエストのたびに自動でテストが走り、失敗があればマージをブロックする仕組みを整えることで、「テストが常に緑の状態」が維持されます。
Xcode Cloud の設定
Xcode Cloud は Apple の公式 CI/CD サービスで、App Store Connect と深く統合されています。月額費用はかかりますが(AppleDeveloper Programに含まれる一定の無料枠あり)、Xcodeプロジェクトとのネイティブな親和性は他のCIサービスにない利点です。特にTestFlight配布やApp Store提出のトリガーとして自動化できる点は、個人開発者にとって大きな価値があります。
Claude Codeはスクリプトの生成や設定の提案に活用できます。
claude "Xcode Cloudのci_post_xcodebuild.sh スクリプトを作成してください。
要件:
- テスト失敗時にSlackへ通知(Webhook URL は環境変数 SLACK_WEBHOOK_URL)
- カバレッジレポートをアーティファクトとして保存
- TestFlight 配布前に自動でバージョン番号をインクリメント"
生成された ci_post_xcodebuild.sh の例:
#!/bin/sh
# ci_post_xcodebuild.sh
set -e
# テスト結果の確認
if [ " $CI_XCODEBUILD_EXIT_CODE " -ne 0 ]; then
echo "❌ Build or tests failed"
# Slack通知
if [ -n " $SLACK_WEBHOOK_URL " ]; then
curl -s -X POST " $SLACK_WEBHOOK_URL " \
-H 'Content-type: application/json' \
--data "{
\" text \" : \" ❌ * $CI_WORKFLOW_NAME * failed on branch \` $CI_BRANCH \`\" ,
\" attachments \" : [{
\" color \" : \" danger \" ,
\" fields \" : [{
\" title \" : \" Commit \" ,
\" value \" : \" $CI_COMMIT \"
}]
}]
}"
fi
exit 1
fi
echo "✅ All tests passed"
# カバレッジレポートの保存
if [ -d " $CI_RESULT_BUNDLE_PATH " ]; then
xcrun xccov view --report " $CI_RESULT_BUNDLE_PATH " > " $CI_DERIVED_DATA_PATH /coverage_report.txt"
echo "Coverage report saved"
fi
GitHub Actions との連携
GitHub Actions を使う場合、Claude Codeのフックと組み合わせると強力なワークフローになります。
# .github/workflows/ios-tests.yml
name : iOS Tests
on :
pull_request :
branches : [ main , develop ]
jobs :
test :
runs-on : macos-15
steps :
- uses : actions/checkout@v4
- name : Select Xcode
run : sudo xcode-select -s /Applications/Xcode_16.3.app
- name : Run Tests
run : |
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
-enableCodeCoverage YES \
-resultBundlePath ./TestResults.xcresult \
| xcbeautify
- name : Coverage Check
run : bash scripts/check_coverage.sh
- name : Upload Test Results
if : always()
uses : actions/upload-artifact@v4
with :
name : test-results
path : TestResults.xcresult
Claude Code Hooks:テスト失敗時の自動修正ループ
Claude Code の Hooks 機能を使うと、テスト実行後に自動でフィードバックループを回せます。
// .claude/hooks.json
{
"PostToolUse" : [
{
"matcher" : "Bash" ,
"hooks" : [
{
"type" : "command" ,
"command" : "bash .claude/scripts/check_test_output.sh"
}
]
}
]
}
#!/bin/bash
# .claude/scripts/check_test_output.sh
# テスト実行後に失敗があればClaudeに自動でフィードバック
LAST_OUTPUT = $( cat /tmp/last_bash_output 2> /dev/null )
if echo " $LAST_OUTPUT " | grep -q "TEST FAILED\|error:" ; then
echo "⚠️ テスト失敗を検出。Claude に修正を依頼します..."
# 失敗メッセージを標準出力に返すことでClaudeに認識させる
echo " $LAST_OUTPUT " | grep -A5 "TEST FAILED\|error:" | head -30
fi
SwiftData テスト:In-Memory モデルコンテナの活用
iOS 17 以降の SwiftData を使ったプロジェクトでは、テスト用のインメモリコンテナを使うことで、本番データベースを汚染せずに永続化ロジックをテストできます。
// SwiftData テスト用ヘルパー
import SwiftData
import Testing
@testable import MyApp
@Suite ( "Article Persistence Tests" )
struct ArticlePersistenceTests {
// テストごとにクリーンなコンテナを使用
var container: ModelContainer
init () throws {
let config = ModelConfiguration ( isStoredInMemoryOnly : true )
container = try ModelContainer ( for : Article. self , configurations : config)
}
@Test ( "記事をCRUD操作できる" )
func articleCRUD () throws {
let context = ModelContext (container)
// Create
let article = Article ( title : "テスト記事" , slug : "test-article" )
context. insert (article)
try context. save ()
// Read
let descriptor = FetchDescriptor < Article > ()
let articles = try context. fetch (descriptor)
#expect (articles. count == 1 )
#expect (articles. first ? .title == "テスト記事" )
// Update
articles. first ? .title = "更新された記事"
try context. save ()
let updated = try context. fetch (descriptor)
#expect (updated. first ? .title == "更新された記事" )
// Delete
if let toDelete = updated. first {
context. delete (toDelete)
try context. save ()
}
let remaining = try context. fetch (descriptor)
#expect (remaining. isEmpty )
}
}
Claude Codeに SwiftData テストの生成を依頼する際は、モデル定義と一緒に渡すと精度が上がります。
claude "Sources/Models/Article.swift の SwiftData モデル定義を読んで、
インメモリコンテナを使ったCRUDテストを ArticlePersistenceTests.swift として生成してください。
リレーション(Article → Tag の多対多)のテストも含めてください"
パフォーマンステスト:XCTest Metrics で計測する
テストは「動作の正確性」を担保するだけでなく、「パフォーマンスのリグレッション」を検知する役割も担えます。XCTest.measure() を使ったパフォーマンステストは、重い処理のベンチマークとして活用できます。
// パフォーマンステストの例
import XCTest
@testable import MyApp
final class ImageProcessingPerformanceTests : XCTestCase {
func testImageFilterPerformance () throws {
let image = UIImage ( named : "test_image_2048x2048" , in : Bundle ( for : type ( of : self )), with : nil ) !
measure ( metrics : [ XCTClockMetric (), XCTMemoryMetric ()]) {
// 計測したい処理
_ = ImageProcessor. applyVintageFilter ( to : image)
}
}
func testArticleListRenderPerformance () throws {
let articles = Article. generateLargeDataset ( count : 1000 )
measure {
// ViewModelのフィルタリング処理を計測
let viewModel = ArticleListViewModel ( articles : articles)
viewModel. filter ( by : "swift" )
}
}
}
ベースラインを設定しておくと、将来の変更でパフォーマンスが10%以上低下した場合にテストが自動で失敗するようになります。Claude Codeには次のように依頼できます。
claude "Sources/Services/ImageProcessor.swift のフィルター処理が
最近のリファクタリング後に遅くなった気がします。
XCTestのパフォーマンステストを作成して、処理時間の計測ベースラインを設定してください"
テスト設計パターン:Builder パターンによる柔軟なテストデータ
テストコードの可読性を高め、メンテナンスコストを下げるための設計パターンとして「Builder パターン」をテストデータ生成に活用する方法を紹介します。
// ArticleBuilder.swift — テスト用ビルダー
@testable import MyApp
struct ArticleBuilder {
private var title: String = "テスト記事"
private var slug: String = "test-article"
private var category: String = "tech"
private var isPremium: Bool = false
private var publishedAt: Date = Date ()
func title ( _ value: String ) -> Self {
var copy = self
copy.title = value
return copy
}
func slug ( _ value: String ) -> Self {
var copy = self
copy.slug = value
return copy
}
func premium () -> Self {
var copy = self
copy.isPremium = true
return copy
}
func publishedDaysAgo ( _ days: Int ) -> Self {
var copy = self
copy.publishedAt = Calendar.current. date ( byAdding : .day, value : - days, to : Date ()) !
return copy
}
func build () -> Article {
Article (
title : title,
slug : slug,
category : category,
isPremium : isPremium,
publishedAt : publishedAt
)
}
}
// 使用例
@Test ( "プレミアム記事のみフィルタリングされる" )
func filterPremiumArticles () {
let articles = [
ArticleBuilder (). build (), // 無料記事
ArticleBuilder (). premium (). build (), // プレミアム記事
ArticleBuilder (). premium (). title ( "別のプレミアム記事" ). build ()
]
let viewModel = ArticleListViewModel ( articles : articles)
viewModel. showPremiumOnly ()
#expect (viewModel.filteredArticles. count == 2 )
}
このビルダーパターンをClaude Codeに生成させると、各エンティティに対して一貫した設計が保たれます。
claude "Sources/Models/ フォルダ内の全 SwiftData モデルに対して、
テスト用の Builder クラスを Tests/Builders/ に生成してください。
既存の Article モデルの ArticleBuilder を参考パターンとして使用してください"
Claude Code × TDD:テスト先行開発をAIで加速する
TDD(テスト駆動開発)は良いと分かっていてもなかなか実践できない、という声をよく聞きます。その最大のハードルは「テストを先に書く手間」です。Claude Codeを使うと、そのハードルを大幅に下げられます。
ステップ1:振る舞いを自然言語で定義する
claude "ArticleFilter サービスに以下の振る舞いが必要です:
- filterByCategory(category: String): 指定カテゴリの記事のみ返す
- filterByLevel(level: Level): beginner/intermediate/advanced でフィルタ
- 複数条件はAND条件(カテゴリ AND レベルが一致するもののみ)
- 結果が0件の場合は空配列を返す(エラーをスローしない)
まずSwift Testingでテストだけ書いてください。実装はまだ不要です。"
ステップ2:テストをレビューして補完する
Claude Codeが生成したテストを確認し、漏れているエッジケース(空配列の入力・nilカテゴリ文字列など)を追加するよう依頼します。
ステップ3:テストを通す実装を依頼する
claude "ArticleFilterTests.swift の全テストが通るように
ArticleFilter.swift を実装してください。
ViewModelへの依存注入ができるようにプロトコルベースで設計してください"
このループを回すことで、テスト設計の思考(「どんな振る舞いが必要か」)に集中しながら、コーディングの手間はClaude Codeに任せられます。
サードパーティSDKのテスト対応:アナリティクスの分離
Firebase Analytics や Mixpanel などのサードパーティSDKをコード内で直接呼び出していると、テストが複雑になります。プロトコルでラップする設計が解決策です。
// アナリティクスを抽象化するプロトコル
protocol AnalyticsTracking {
func track ( event : String , parameters : [ String : Any ])
}
// テスト用モック:実際のSDKを呼ばずに記録だけする
final class MockAnalyticsTracker : AnalyticsTracking {
var trackedEvents: [(name: String , params: [ String : Any ])] = []
func track ( event : String , parameters : [ String : Any ]) {
trackedEvents. append (( name : event, params : parameters))
}
}
// テスト
@Test ( "記事閲覧時にアナリティクスイベントが送信される" )
func articleViewTracksEvent () async throws {
let mockTracker = MockAnalyticsTracker ()
let viewModel = ArticleViewModel ( analytics : mockTracker)
await viewModel. markAsViewed ( articleId : "article-123" )
#expect (mockTracker.trackedEvents. count == 1 )
#expect (mockTracker.trackedEvents. first ? .name == "article_viewed" )
}
Claude Codeに既存コードの監査を依頼することで、プロトコル化が必要な箇所を一括で洗い出せます。
claude "Sources/ 配下でテストされていないアナリティクス呼び出しを全て特定してください。
ファイル名と行番号のリストを出力し、プロトコルインジェクションで
テスト可能にする方法を提案してください"
よくあるエラーと対処法
エラー1: 非同期テストでタイムアウトする
// ❌ 悪い例: sleep で待機
func testAsyncOperation () async throws {
viewModel. startFetch ()
try await Task. sleep ( nanoseconds : 2_000_000_000 ) // 2秒待機
XCTAssertTrue (viewModel.isLoaded) // 不安定
}
// ✅ 良い例: confirmation で明示的に待機
@Test func testAsyncOperation () async throws {
await confirmation ( "データが読み込まれる" ) { confirm in
viewModel.onLoadComplete = { confirm () }
await viewModel. startFetch ()
}
#expect (viewModel.isLoaded)
}
エラー2: Swift Actor の isolation エラー
// ❌ Actor の状態にメインスレッド外からアクセス
@Test func testActorState () async throws {
let actor = MyActor ()
await actor. performWork ()
XCTAssertEqual (actor.state, .completed) // ❌ Isolation violation
}
// ✅ await でアクセス
@Test func testActorState () async throws {
let actor = MyActor ()
await actor. performWork ()
let state = await actor.state // ✅
#expect (state == .completed)
}
エラー3: UI テストで要素が見つからない
# Claude Code に診断を依頼
claude "XCUITest で 'login_submit_button' が見つからないエラーが発生しています。
LoginView.swift のアクセシビリティID設定と、
UIテストの waitForExistence タイムアウト設定を確認して修正案を提示してください"
テスト文化の定着:個人開発者のための現実的なアプローチ
「テストを書く文化を作りたい」という気持ちはあっても、締め切りが迫ると後回しになる——これは個人開発者が特に陥りやすい罠です。チームと異なり、テストを書かなくても誰かに注意されることがありません。自律的にテスト習慣を維持するために、次のようなアプローチが効果的です。
「新機能1件につき最低3テスト」ルールの設定 。厳格なカバレッジ閾値を設けるよりも、「何か新しい機能を実装したら、最低でも正常系1件・異常系1件・境界値1件のテストを書く」というシンプルなルールの方が長続きします。Claude Codeを使えば、実装コードを貼り付けて「このコードの最低限のテストを3件書いて」と依頼するだけで30秒以内に完了します。
バグ修正時にテストを追加する「Regression Test」の習慣 。バグが見つかったとき、修正する前にそのバグを再現するテストを書きます。テストが失敗することを確認してから修正し、テストが通ることを確認してコミットします。このフローを繰り返すことで、同じバグが再発するリスクを自然に下げられます。
テスト実行時間のモニタリング 。テストが遅くなると書くモチベーションが下がります。テストスイート全体が30秒以内に完了することを目標にしましょう。遅いテストはClaude Codeに「このテストスイートで実行に2秒以上かかるテストを特定して、高速化の提案をしてください」と分析を依頼できます。
週1回のカバレッジレポート確認 。前週と比べてカバレッジが下がっていたら原因を確認する、というシンプルなルーティンで、品質への意識を継続的に保てます。
これらの習慣はどれも「完璧なテスト」を目指すものではなく、「継続できるテスト」を目指すものです。完璧主義にならずに始めることが、長期的な品質向上への一番の近道です。
全体を振り返ってと次のステップ
要点をまとめると次の通りです。
CLAUDE.md の整備 : アーキテクチャ・テスト方針・命名規則をClaude Codeに伝えることで、プロジェクト固有の高品質なテストを生成できる
Swift Testing を優先採用 : Xcode 16以降のプロジェクトでは import Testing ベースで書き、既存XCTestとの共存も可能
Critical Pathに絞ったUIテスト : アクセシビリティIDを活用し、ログイン・購入フローなど本当に重要な操作のみ自動化する
カバレッジ80%以上を品質ゲートに : CI/CDパイプラインで閾値を下回るPRをブロックする
Hooksによる自動修正ループ : テスト失敗時にClaude Codeが自動で原因分析・修正を試みる仕組みを整える
テスト基盤の次のステップとして、Claude Code Hooks 自動化ガイド や Claude Code × App Store Connect リリース自動化 も合わせてご参照ください。
iOSアプリ開発のテスト戦略をさらに体系的に