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):

// ファイルパス: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):

// ファイルパス: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 / isNotNullnull を検証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 の実際の応用:

import 'package:test/test.dart';

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!

例外のテスト

例

import 'package:test/test.dart';

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() は関連するテストケースをまとめて、テスト構造をより明確にするために使用します。

例

import 'package:test/test.dart';

// テスト対象の関数
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 の後に実行されます。

これらはテスト環境の準備とリソースのクリーンアップに使用されます。

例

import 'package:test/test.dart';

// 初期化とクリーンアップが必要なクラスをシミュレート
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 の完了を待ちます。

例

import 'package:test/test.dart';

// 非同期関数:ネットワークリクエストをシミュレート
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開発に応用できます。

その他の拡張