単体テストは、システムの他の部分から隔離して個々のモジュールやコード関数の正確性をテストするソフトウェア検証手法です。Martin Fowler, 2026によると、単体テストはCI/CDとリファクタリングの基盤であり、コードの動作に関する迅速なフィードバックを提供します。モジュールテストは開発の初期段階でエラーを発見し、修正コストを大幅に削減します。
重要なポイント
単体テストは、ソースコードの個々のユニット(関数、メソッド、クラス)をプログラムの他の部分から隔離して検証するプロセスです。各テストはモジュールの特定の使用シナリオを実行し、結果が期待値と一致するかを確認します。単体テストはメインコードと同じプログラミング言語で記述され、開発環境またはCI/CDパイプラインで自動実行されます。統合テストとは異なり、単体テストは実際のデータベース、ファイルシステム、ネットワークサービスと相互作用しません。
主な目的は、変更後のコードの正確性に関する迅速なフィードバックです。開発者がメソッドをリファクタリングする場合、単体テストスイートは動作が壊れていないことを確認します。Google Testing Blog(2025)によると、単体テストのカバレッジが60%を超えるプロジェクトでは、本番インシデントが2.5分の1に減少します。追加の利点:コードのドキュメント化(テストはAPIの使用方法を示す)、リファクタリングの簡素化(動作を維持しながら実装を変更可能)、迅速な回帰診断。
すべての自動テストが単体テストとは限りません。基準:単一モジュール(クラスまたは関数)をテストし、外部依存関係をモックまたはスタブで置き換え、テストはミリ秒で実行され、サーバーやデータベースの起動を必要としません。実際のデータベースにアクセスするテストは統合テストです。ブラウザを開くテストはE2Eテストです。テストタイプ間の境界を理解することは、テストピラミッドで適切にリソースを配分するために重要です。
品質の高い単体テストは、Robert C. Martinが提唱したFIRSTの原則に従います。各テストはFast(高速 — ミリ秒)、Isolated(分離 — 他のテストに依存しない)、Repeatable(再現可能 — どのマシンでも同じ結果)、Self-validating(自己検証 — 結果は「合格」または「不合格」、手動確認不要)、Timely(タイムリー — コードの前または同時に記述)であるべきです。いずれかの原則に違反すると、テストの価値が低下します。
単体テストを記述するための標準テンプレートです。Arrange — データと依存関係の準備:オブジェクトの作成、モックの設定、入力パラメータの指定。Act — テスト対象のアクションの実行:メソッドまたは関数の呼び出し。Assert — 結果の検証:実際の値と期待値の比較。3つのブロックに分割することで、テストが読みやすく理解しやすくなります。Assertブロックが複雑なロジックを必要とする場合、テストは一度に多くのことを確認しすぎている可能性があります。
// KotlinでJUnit 5を使用したAAAパターンの単体テスト例
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — テスト対象オブジェクトを作成
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — アクションを実行
val result = calculator.add(2, 3)
// ASSERT — 結果を検証
Assertions.assertEquals(5, result)
}
}
テスト名は、何をテストし、どのような結果が期待されるかを記述する必要があります。形式:[methodName]_[scenario]_[expectedResult]。例:calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal。適切なテスト名はコメントの代わりとなり、失敗時にどの機能が壊れているかを即座に示します。test1、checkSomething、verifyのような名前は避けてください — 情報を持たず、診断を複雑にします。
テスト対象モジュールを外部依存関係から分離するために、テストダブルが使用されます。主要なタイプ:モック(mocks) — 特定のメソッドが期待されるパラメータで呼び出されたことを検証;スタブ(stubs) — メソッド呼び出し時に定義済みの値を返す;フェイク(fakes) — 実際のコンポーネントの簡略化された実装(例:データベースと連携するUserRepositoryの代わりにInMemoryUserRepository)。選択は、状態(スタブ)と相互作用(モック)のどちらを検証する必要があるかに依存します。
| ダブル | 検証内容 | 例 |
|---|---|---|
| モック | 正しいパラメータでのメソッド呼び出し | userRepository.save(user)が正確に1回呼び出された |
| スタブ | 戻り値 | repository.findById(1)がUser(id=1, name="Test")を返す |
| フェイク | 簡略化された実装によるロジック | DBの代わりにHashMapを使うInMemoryMapUserRepository |
| スパイ | 実際のオブジェクトの部分モック | spy(repo).when(findById).thenReturn(user) |
MockitoはJavaとKotlinで最も人気のあるモッキングフレームワークです。mock()によるモックの作成、when().thenReturn()による戻り値の設定、verify()による呼び出しの検証が可能です。Mockito(5.x)の最新バージョンは、静的モック(mockStatic)とBDDMockito(given-willReturn)による簡略化された構文をサポートしています。重要なルール:自分が所有していないものをモックしないでください — 値オブジェクトや標準ライブラリのモックは作成しないでください。
// KotlinでMockitoを使用した単体テスト例
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD(テスト駆動開発)は、コードの実装前にテストを記述する手法です。「レッド・グリーン・リファクタ」のサイクル:失敗するテストを書き(レッド)、テストを通す最小限のコードを書き(グリーン)、動作を変えずにコードを改善します(リファクタ)。TDDはすべてのコードがテストでカバーされ(実装された機能のカバレッジ100%)、コードがテスト可能であることを保証します — コードのテストが困難な場合、アーキテクチャの改善が必要です。
IBM(2006-2026、縦断研究)の調査によると、TDDを使用するチームは、テストをコードの後で書くチームと比較して、本番環境での欠陥が40〜80%少なくなります。TDDはアーキテクチャも改善します:開発者は実装前にAPI設計について考える必要があり、疎結合と高凝集につながります。追加の効果として生きたドキュメントがあります:テストはモジュールの動作仕様として機能し、常に最新の状態に保たれます。
TDDは常に最適とは限りません。UIコンポーネントは隔離してテストするのが難しく、スナップショットテストやビジュアルリグレッションテスト(Percy、Chromatic)の方が効果的です。プロトタイピングや調査(スパイクソリューション)にはテストは不要です。テストのないレガシーコードをTDDでカバーするのは困難です — その場合はまず特性テスト(リファクタリング前に現在の動作をキャプチャするテスト)が必要です。これらの場合、TDDは完全に放棄されるのではなく適応されます — レガシーコード全体ではなく、変更された機能に対してテストが記述されます。
モバイル開発には特殊性があります:ビジネスロジックがUIコード(Activity、ViewController、ViewModel)と混在することが多く、単体テストが複雑になります。最良のプラクティスは薄いView、厚いViewModel:すべてのロジックをUIコンポーネントから分離したクラス(UseCase、Repository、ViewModel)に抽出し、エミュレータなしで簡単にテストできるようにします。AndroidとiOSには、デバイスを起動せずにJVM/Nativeで動作するネイティブの単体テストフレームワークがあります。
Androidの単体テストはエミュレータなしでローカルのJVMで実行され、実行速度を提供します — 通常のテストは100ms未満です。JUnit 5がメインランナーです。ViewModelテストでは、コルーチンテストにkotlinx-coroutines-test、StateFlowテストにTurbineを使用します。Robolectricは、シャドウクラスをロードすることでエミュレータなしでAndroid依存コンポーネント(Context、Resources)をテストできます。ComposeテストにはCompose UI Testを使用します — ただし、これらはUIテストであり、単体テストではありません。
iOSの単体テストはSwiftでXCTest(Xcodeに組み込み)を使用して記述されます。Quick + Nimbleは、より読みやすいテスト(describe/context/it)のためのBDDフレームワークです。モッキングにはCuckoo(モック生成)またはSwiftyMockyを使用します。Swiftはプロトコルと依存性注入をサポートしており、依存関係の置き換えを容易にします。重要なポイント:iOSの単体テストは実際のデバイスではなく、macOSシミュレータで実行されます。ハードウェア機能(カメラ、Bluetooth)を必要とするテストは統合テストです。
Flutterの単体テストはflutter_testパッケージを使用し、エミュレータなしでDart VM上で実行されます。モッキングには、コード生成(build_runner)とともにmockitoパッケージを使用します。ウィジェットテスト(同じパッケージ内)は個々のウィジェットをテストしますが、レンダリングが必要で低速です — UIロジックの検証にのみ使用してください。純粋なDartロジック(モデル、リポジトリ、ブロック)は、flutter_testをインポートせずに通常のDartテストとしてテストされます。
// Flutterでmockitoを使用した単体テスト例
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
効果的な単体テストには規律が必要です。主なルール:動作をテストし、実装をテストしない。テストはモジュールが内部でどのように実装されているか(どのプライベートメソッドがどの順序で呼び出されるか)を知るべきではありません。テストが実装に結びついていると、リファクタリングのたびに壊れ、価値を失います。テストは契約を検証します:入力Xに対して、出力Yであるべきです。例外は、呼び出し順序が重要なパフォーマンスが重要なアルゴリズムのテストです。
100%のカバレッジは達成不可能で不要な目標です。Google Testing Blog(2025)によると、単体テストの最適なカバレッジレベルはコード行の70〜80%です。100%のカバレッジはゲッター、セッター、コンストラクタをテストすることで達成されることが多く、価値を追加しません。クリティカルなビジネスロジックに焦点を当ててください:複雑な計算、バリデーション、エラーハンドリング、エッジケース。測定にはJaCoCo(Java)、Coverage.py(Python)、Istanbul(JS)を使用し、CIにしきい値を設定します — カバレッジが60%未満でビルド失敗。
単体テストはCI/CDパイプラインの最初のステージです。ビルドとデプロイの前に、リポジトリへのプッシュごとに実行されます。単体テストスイートの平均実行時間は5分を超えてはいけません — それを超えるとテストは「高速」ではなくなり、開発者はローカルで実行しなくなります。テストを高速(単体)と低速(統合)に分け、パイプラインの異なるステージで実行します。高速化のために並列実行とフェイルファストを使用してください。
よくある質問
単体テストは外部依存関係をモックで置き換えて単一モジュールを隔離して検証します。統合テストは複数の実際のコンポーネント(DB、API、ファイルシステム)間の相互作用を検証します。単体テストはミリ秒で実行され、統合テストは秒で実行されます。テストピラミッドでは、単体テストが70%を占めます。
選択はプラットフォームによって異なります:Java/KotlinにはJUnit 5、iOS/SwiftにはXCTest、Pythonにはpytest、JavaScript/TypeScriptにはJest/Vitest、Flutterにはflutter_test。モッキングにはMockito(Java)、Cuckoo(iOS)、unittest.mock(Python)、vitest.mock(JS)を使用します。最新のフレームワークはすべて、パラメータ化テスト、組み込みアサーション、並列実行をサポートしています。
Fast — テストはミリ秒で実行。Isolated — 他のテストや外部システムに依存しない。Repeatable — どのマシンでも同じ結果。Self-validating — 自動的に結果を検証。Timely — コードの前または同時に記述。1つの原則でも違反するとテストの効果が低下します。
はい、必須です。ViewModelにはビジネスロジック(イベント処理、データ変換、状態管理)が含まれます。Androidでは、コルーチンテストにkotlinx-coroutines-test、StateFlowテストにTurbineを使用します。iOSでは、ViewModelのCombine Publishersやasync/awaitをテストします。ViewModelテストは、エミュレータなしでJVM/macOS上で実行される純粋な単体テストです。
単体テストではネットワークリクエストは実行されません — HTTPクライアントのモックに置き換えられます。AndroidではMockWebServer(OkHttp)を使用します — これはローカルHTTPサーバーを起動し、実際のネットワーク相互作用を再現するためモックより優れています。MockWebServerは現実性を失わずに分離を提供します。iOSでは — OHHTTPStubsまたはURLProtocolを使用してレスポンスをインターセプトして置き換えます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。