Dart 単体テスト
単体テストはコード品質を保証する基本的な手段です。
それは、最小の機能単位(通常は関数またはメソッド)の動作が期待どおりかどうかを検証します。
この章では、Dart の test パッケージの使い方、test() テストケースの作成、expect アサーション、group() によるグループ化テストを紹介します。
test パッケージのインストールと設定
Dart 公式はテストを作成・実行するための test パッケージを提供しています。
まず pubspec.yaml に依存関係を追加します:
# 文件路径:pubspec.yaml dev_dependencies: test: ^1.24.0
次に以下のコマンドを実行してインストールします:
$ dart pub get
テストファイルは通常、プロジェクトの test/ ディレクトリに置き、ファイル名は _test.dart で終わります。
典型的なプロジェクト構造:
my_project/ ├── lib/ │ └── calculator.dart # 被测试的代码 ├── test/ │ └── calculator_test.dart # 测试文件 └── pubspec.yaml
test() テストケースの作成
test() 関数はテストケースを作成するための基本単位です。
これは2つの引数を受け取ります:テストの説明(文字列)とテスト関数本体です。
例
テスト対象のコード(lib/calculator.dart):
class Calculator {
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
int multiply(int a, int b) => a * b;
double divide(int a, int b) {
if (b == 0) {
throw ArgumentError('除数を 0 にできません');
}
return a / b;
}
bool isEven(int n) => n % 2 == 0;
List<int> filterPositive(List<int> numbers) {
return numbers.where((n) => n > 0).toList();
}
}
例
テストファイル(test/calculator_test.dart):
import 'package:test/test.dart';
import 'package:my_project/calculator.dart';
void main() {
// テスト用の Calculator インスタンスを作成
var calculator = Calculator();
// test() 関数:第1引数はテストの説明、第2引数はテスト関数
test('add() 2つの正数の加算', () {
var result = calculator.add(3, 5);
// expect() アサーション:実際の結果が期待値と等しいかを検証
expect(result, equals(8));
});
test('add() 負数を含む加算', () {
expect(calculator.add(-3, 5), equals(2));
expect(calculator.add(-3, -5), equals(-8));
});
test('subtract() 減算', () {
expect(calculator.subtract(10, 3), equals(7));
});
test('multiply() 乗算', () {
expect(calculator.multiply(4, 5), equals(20));
// ゼロを掛ける
expect(calculator.multiply(100, 0), equals(0));
});
test('divide() 正常な除算', () {
expect(calculator.divide(10, 2), equals(5.0));
expect(calculator.divide(7, 2), equals(3.5));
});
test('isEven() 偶数判定', () {
expect(calculator.isEven(4), isTrue);
expect(calculator.isEven(5), isFalse);
});
}
テストを実行:
$ dart test
00:00 +6: All tests passed!
expect アサーション
expect() はテストで最も中心的な関数であり、実際の値が特定の条件を満たすかどうかを検証します。
基本構文:expect(actual, matcher)。
よく使う Matcher
| Matcher | 用途 | 例 |
|---|---|---|
| equals(expected) | 値が等しいことを検証 | expect(result, equals(42)) |
| isTrue / isFalse | ブール値を検証 | expect(flag, isTrue) |
| isNull / isNotNull | null を検証 | expect(value, isNull) |
| contains(value) | 要素(リスト)または部分文字列(文字列)を含むことを検証 | expect(list, contains('a')) |
| isA | 型を検証 | expect(obj, isA |
| throws | 例外がスローされることを検証 | expect(() => f(), throwsException) |
| isNotEmpty | 空でないこと | expect(list, isNotEmpty) |
| hasLength(n) | 長さを検証 | expect(list, hasLength(3)) |
| greaterThan(n) | より大きい | expect(score, greaterThan(60)) |
| closeTo(value, delta) | 浮動小数点数の近似等価 | expect(3.14, closeTo(3.1, 0.1)) |
例
さまざまな Matcher の実際の応用:
void main() {
test('サンプル 各種アサーションの例', () {
// 基本的な等価
expect(2 + 2, equals(4));
// ブール値
expect('hello'.contains('h'), isTrue);
expect(''.isEmpty, isTrue);
// null チェック
String? name;
expect(name, isNull);
name = 'サンプル';
expect(name, isNotNull);
// 型チェック
expect('サンプル', isA<String>());
expect(42, isA<int>());
// リスト/文字列の包含
expect([1, 2, 3], contains(2));
expect('Hello, サンプル!', contains('サンプル'));
// 長さ
expect([1, 2, 3], hasLength(3));
expect('Dart', hasLength(4));
// 数値比較
expect(100, greaterThan(50));
expect(30, lessThan(60));
expect(75, greaterThanOrEqualTo(60));
// 浮動小数点数の比較(精度問題を避ける)
expect(0.1 + 0.2, closeTo(0.3, 0.001));
// コレクションが空でない
expect([1, 2], isNotEmpty);
// リストの等価
expect([1, 2, 3], equals([1, 2, 3]));
// Map が特定のキーを含む
var user = {'name': 'サンプル', 'age': 10};
expect(user, containsPair('name', 'サンプル'));
expect(user.keys, contains('age'));
});
}
00:00 +1: All tests passed!
例外のテスト
例
int divide(int a, int b) {
if (b == 0) throw ArgumentError('除数を 0 にできません');
return a ~/ b;
}
void main() {
test('divide() ゼロ除算は例外をスローする', () {
// 任意の例外がスローされることを検証
expect(() => divide(10, 0), throwsException);
// 特定の型の例外がスローされることを検証
expect(() => divide(10, 0), throwsArgumentError);
// 例外メッセージを検証
expect(
() => divide(10, 0),
throwsA(predicate((e) =>
e is ArgumentError &&
e.message.contains('除数'))),
);
});
test('divide() 正常時は例外をスローしない', () {
expect(() => divide(10, 2), returnsNormally);
expect(divide(10, 2), equals(5));
});
}
00:00 +2: All tests passed!
例外をテストする場合、例外をスローする可能性のあるコードを匿名関数(() => code)でラップする必要があり、直接呼び出してはいけません。直接 expect(divide(10, 0), throwsException) と書くと、divide が即座に例外をスローし、expect が実行される機会がありません。
グループ化テスト group()
group() は関連するテストケースをまとめて、テスト構造をより明確にするために使用します。
例
// テスト対象の関数
class StringUtils {
static String capitalize(String s) {
if (s.isEmpty) return s;
return s[0].toUpperCase() + s.substring(1);
}
static String reverse(String s) {
return s.split('').reversed.join('');
}
static bool isPalindrome(String s) {
var clean = s.toLowerCase().replaceAll(' ', '');
return clean == reverse(clean);
}
static int countWords(String s) {
if (s.trim().isEmpty) return 0;
return s.trim().split(RegExp(r'\s+')).length;
}
}
void main() {
// group() をネストしてテストを整理
group('StringUtils', () {
// サブグループ
group('capitalize()', () {
test('通常の単語の先頭文字を大文字にする', () {
expect(StringUtils.capitalize('hello'), equals('Hello'));
});
test('すでに大文字の単語は変わらない', () {
expect(StringUtils.capitalize('Hello'), equals('Hello'));
});
test('空文字列は空文字列を返す', () {
expect(StringUtils.capitalize(''), equals(''));
});
test('1文字', () {
expect(StringUtils.capitalize('a'), equals('A'));
});
});
group('reverse()', () {
test('通常の文字列を反転', () {
expect(StringUtils.reverse('サンプル'), equals('BOONUR'));
});
test('回文を反転しても変わらない', () {
expect(StringUtils.reverse('aba'), equals('aba'));
});
test('空文字列', () {
expect(StringUtils.reverse(''), equals(''));
});
});
group('isPalindrome()', () {
test('回文は true を返す', () {
expect(StringUtils.isPalindrome('racecar'), isTrue);
expect(StringUtils.isPalindrome('A man a plan a canal Panama'), isTrue);
});
test('回文でない文字列は false を返す', () {
expect(StringUtils.isPalindrome('hello'), isFalse);
});
test('空文字列は回文とみなす', () {
expect(StringUtils.isPalindrome(''), isTrue);
});
});
group('countWords()', () {
test('通常の文', () {
expect(StringUtils.countWords('Hello World Dart'), equals(3));
});
test('余分な空白', () {
expect(StringUtils.countWords(' Hello World '), equals(2));
});
test('空文字列', () {
expect(StringUtils.countWords(''), equals(0));
expect(StringUtils.countWords(' '), equals(0));
});
});
});
}
00:00 +12: All tests passed!
group() は入れ子にでき、階層的なテスト構造を作成できます。
これによりテストレポートが読みやすくなり、失敗したテストをすばやく特定しやすくなります。
良いグループ化の戦略は、「テスト対象のモジュール/クラス/メソッド」ごとに整理することです。そうすれば、テストが失敗したとき、どの機能に問題があるかをすぐに把握できます。
setUp と tearDown
setUp は各 test の前に実行され、tearDown は各 test の後に実行されます。
これらはテスト環境の準備とリソースのクリーンアップに使用されます。
例
// 初期化とクリーンアップが必要なクラスをシミュレート
class Database {
bool isConnected = false;
void connect() {
isConnected = true;
print(' データベース接続済み');
}
void disconnect() {
isConnected = false;
print(' データベース接続が切断されました');
}
String query(String sql) {
if (!isConnected) throw Exception('データベース未接続');
return 'クエリ結果: $sql';
}
}
void main() {
group('Database テスト', () {
late Database db; // late 遅延初期化
// 各 test の前に実行
setUp(() {
db = Database();
db.connect();
print(' [setUp] テスト環境を準備');
});
// 各 test の後に実行
tearDown(() {
db.disconnect();
print(' [tearDown] テスト環境をクリーンアップ');
});
test('query() 正常なクエリ', () {
var result = db.query('SELECT * FROM users');
expect(result, contains('サンプル') ? result.contains('users') : result.contains('users'));
// 簡略化したアサーション
expect(result, isNotEmpty);
});
test('query() 接続後は複数回クエリを実行できる', () {
var r1 = db.query('SELECT 1');
var r2 = db.query('SELECT 2');
expect(r1, isNotEmpty);
expect(r2, isNotEmpty);
});
test('未接続時のクエリは例外をスローする', () {
db.disconnect(); // 手動で切断
expect(() => db.query('SELECT 1'), throwsException);
});
});
}
数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 00:00 +3: All tests passed!
出力内の setUp と tearDown の実行順序に注意してください——各 test の前後に1回ずつ実行されます。
setUp と tearDown は、各テストが他のテストの影響を受けず、クリーンな状態から開始されることを保証します。これはテストの独立性を確保するための鍵です。
非同期テスト
Dart の test パッケージは非同期テストをネイティブにサポートしています。
テスト関数は Future を返すことができ、フレームワークは自動的に Future の完了を待ちます。
例
// 非同期関数:ネットワークリクエストをシミュレート
Future<String> fetchUserData(int userId) async {
await Future.delayed(Duration(milliseconds: 100));
if (userId <= 0) {
throw Exception('無効なユーザー ID');
}
return 'ユーザー $userId のデータ';
}
Future<List<int>> fetchNumbers() async {
await Future.delayed(Duration(milliseconds: 50));
return [1, 2, 3, 4, 5];
}
void main() {
group('非同期テスト', () {
test('fetchUserData() 正常にデータを取得', () async {
var data = await fetchUserData(1);
expect(data, equals('ユーザー 1 のデータ'));
});
test('fetchUserData() 無効な ID で例外をスロー', () async {
expect(
() => fetchUserData(-1),
throwsA(isA<Exception>()),
);
});
test('fetchNumbers() 正しいリストを返す', () async {
var numbers = await fetchNumbers();
expect(numbers, hasLength(5));
expect(numbers, contains(3));
expect(numbers.first, equals(1));
});
// 複数の非同期操作のテスト
test('連続する非同期操作', () async {
var data1 = await fetchUserData(1);
var data2 = await fetchUserData(2);
expect(data1, isNot(equals(data2)));
expect(data1, contains('1'));
expect(data2, contains('2'));
});
});
}
00:00 +4: All tests passed!
テストを実行する一般的なコマンド
| コマンド | 機能 |
|---|---|
| dart test | すべてのテストを実行 |
| dart test test/calculator_test.dart | 指定したテストファイルを実行 |
| dart test --name="add" | 名前に "add" を含むテストのみ実行 |
| dart test --concurrency=4 | テストを並行実行(高速化) |
| dart test --reporter=expanded | 詳細出力モード |
| dart test --coverage=coverage | テストカバレッジデータの生成 |
テストのベストプラクティス
| 実践 | 説明 |
|---|---|
| 各テストは1つのことだけを検証する | 1つのtest()が1つの動作に対応し、失敗時の特定が速くなる |
| テスト名は実装ではなく動作を説明する | 「add() 2つの正数を加算」は「add() テスト」よりも優れている |
| AAAパターン | Arrange(準備)→ Act(実行)→ Assert(検証) |
| 最初に失敗するテストを書く | テストがエラーを捕捉できることを確認してから、それを通過させるコードを書く |
| 境界条件のテスト | 空値、ゼロ、負数、最大値、最小値 |
| テスト間の独立性を保つ | あるテストの結果が別のテストに影響を与えてはならない |
テストは負担ではなく保険です。テストを多く書くほど、リファクタリング時の自信が高まります。テストが失敗したとき、どこに問題があるのか推測する必要はありません。テストが正確に教えてくれます。
本章のまとめ
本章では、Dartユニットテストの完全な流れを紹介しました:testパッケージのインストール、test()テストケースの作成、expectアサーションの使用、group()によるグループ化、setUp/tearDownによる環境管理、そして非同期テストです。
ユニットテストはプロの開発者にとって必修科目です。テストを書く習慣を身につけることで、コードの品質は継続的に向上します。
チュートリアル全体のまとめ
Dartプログラミング言語の入門学習の完了おめでとうございます!
この21章の学習の道のりを振り返ってみましょう:
| 段階 | 章 | 核心内容 |
|---|---|---|
| 一、入門基礎 | 第1〜4章 | Dart概要、環境構築、最初のプログラム、基本文法 |
| 二、核心型と制御フロー | 第5〜8章 | 変数とデータ型、演算子、制御フロー、コレクション |
| 三、関数とオブジェクト指向 | 第9〜13章 | 関数、クラスとオブジェクト、継承とポリモーフィズム、インターフェースとMixin、ジェネリクス |
| 四、発展的な機能 | 第14〜17章 | 列挙型とシンボル、例外処理、パッケージとライブラリ管理、typedef |
| 五、非同期プログラミングとテスト | 第18〜21章 | 非同期プログラミング、Stream、並行処理とIsolate、ユニットテスト |
これらの内容を習得したことで、Dartを使用した日常的な開発の能力が身につきました。
次のステップとして深く学べるのはFlutterフレームワークで、Dartの知識をモバイル端末とWeb端末のUI開発に応用できます。
その他の拡張