【Flutter】ユニットテスト基本のき

## はじめに

みなさん、テストしてますか？
私の現場ではテストはマストです😇
実装と合わせてテストも必ず書いてプルリクエストを提出しています。

初学者や実務未経験者が必ずと言っていいほどつまずくのがここだと思います。
いろいろな記事を見ながら学習してみてはつまずき、現場で書いて泣きました😢

そんな苦労を無駄にしないためにも、自分自身が忘れないようにするためにも記事に書き起こしていこうと思います。

### 記事の対象者

- unit_testについて知りたい方
- unit_testを習得したい方

### 記事を執筆時点での筆者の環境

```

[✓] Flutter (Channel stable, 3.22.1, on macOS 14.3.1 23D60 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.2)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.90.1)

```

### ソースコード

https://github.com/HaruhikoMotokawa/shared_preference_sample

:::note
テストコードについて
今回紹介するテンプレートコードやサンプルコード意外にも
実際に実装した内容に対するテストコードを鋭意作成中です💪
ひとまず動くテストしかpushはしていないので、気になる方は覗いてみてください👀

※ 学習中なので、内容は変更される場合があります🙇‍♂️
:::

## 1. テストってなあに？

ソフトウェア開発におけるテストとは、プログラムが期待通りに動作するかを確認するためのプロセスです。
これには、バグやエラーの発見、機能の検証、パフォーマンスの評価などが含まれます。
テストにはいくつかの種類があり、以下のようなものがあります

1.	**単体テスト（Unit Testing）**：
プログラムの個々の部分（関数やメソッドなど）をテストします
2.	**ウィジェットテスト(Widget Testing)**：UIコンポーネントであるWidgetを個別に、または複数組み合わせた形でテストします
3.	**結合テスト（Integration Testing）**：
複数の部分が組み合わさったときに正しく動作するかをテストします
3.	**システムテスト（System Testing）**：
システム全体が要求仕様通りに動作するかをテストします
4.	**受け入れテスト（Acceptance Testing）**：
ユーザーや顧客がシステムを受け入れるかどうかを確認するためのテストです

エンジニアでは主に１、２、３を行います。
２はFlutter特有のテストですね。
４はQAチーム、または外部のQA会社に委託する場合が多いと思います。
５はいわゆるユーザーにテストしてもらうものでβテストなどです。

今回はそんな中でFlutterにおける単体テストについてお話ししていきます。

## 2. ユニットテストの準備

### 2-1. フォルダとファイルの作成

テストのファイルはtestフォルダー内に作成します。
原則はテスト対象のファイルが置かれているディレクトリ構成に則ってディレクトリとテストファイルを作成します。
そのほうが探す際にわかりやすいからです。

例えば以下のrepository.dartとprovider.dartのテストを書きたいとなった場合を見てみましょう。

```

lib
├── data
│   ├── local_sources
│   │   ├── shared_preference.dart
│   │   └── shared_preference.g.dart
│   └── repositories
│       └── key_value_repository
│           ├── provider.dart // <= 💡 ここをテストしたい
│           ├── provider.g.dart
│           └── repository.dart // <= 💡 ここをテストしたい

```

その場合は以下のようにディレクトリを作り、ファイルを作成します。

```

test
├── data
│   └── repositories
│       └── key_value_repository
│           ├── provider_test.dart // <= 😀
│           └── repository_test.dart // <= 😀

```

スクショで見るとこのような形になります。

![スクリーンショット 2024-06-22 18.13.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/baccb330-9daa-56e4-a33f-be4787dc6c3b.png)

:::note warn
テストファイルは**名称の末尾を必ず_test.dart**としないとテスト用のファイルだと認識されずにテストが実行できません。
ファイル作成後は必ず確認しましょう！
:::

今回は実際の実装内容は使わずサンプルを使って説明していきます。
```

test
└── sample
    ├── sample_test.dart
    └── unit_test_template_test.dart

```
### 2-2. 実行環境
次にテストファイル内の実行環境を作成します。
私が作成したテンプレートをみながら解説します。

```test/sample/unit_test_template_test.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  late ProviderContainer container;

  setUp(() {
    container = ProviderContainer();
  });
  tearDown(() {
    container.dispose();
  });
  // TODO: 使用するときにskipを外す
  group('apple test', skip: true, () {
    test('titleができる', () {});
    test('titleができる', () {});
    test('titleができる', () {});
  });
  // TODO: 使用するときにskipを外す
  group('orange test', skip: true, () {
    test('titleができる', () {});
    test('titleができる', () {});
    test('titleができる', () {});
  });
}


```
#### `main()`
まずはプログラムを実行する上で欠かせない、`main()`関数です。
この中に必要な関数を書いていきます。

#### 定数
テスト全体で使う定数があるのであれば最初に定義します。
このテンプレートには`late ProviderContainer container;`を定義しています。
ほとんどのプロジェクトではRiverpodを使用していると思いますのでテンプレートにも載せました。
当然、Riverpodを使っていないプロジェクトまたはファイルものあるので、必要なければ記載しなくて大丈夫です。

#### `setUp()`

```dart

 setUp(() {
    container = ProviderContainer();
  });
  
```

この関数は後に出てくる個々の`test()`実行前に呼ばれます。
主な用途はテストごとの設定を行います。
よくあるのは`ProviderContainer`やダミーデータ、mockのスタブを設定するのに使用します。
引数が関数になっているので使用する際はその無名関数内に処理を書きます。

尚`main()`関数の直下に配置すると全てのテストが実行される前に実行されます。
また、後述する`group()`内に記述するとその`group()`内のテストが実行される前に実行されます。

#### `tearDown()`

```dart

  tearDown(() {
    container.dispose();
  });

```

この関数は`setUp()`とは反対にそれぞれのテストが終わる直前に呼び出されます。
主な用途はリソースの解放です。
よくあるのは`ProviderContainer`の解放、`StreamController`の購読終了などです。

尚こちらも`setUp()`と同じく`main()`関数の直下に配置すると全てのテストの終了直前に実行されます。
また、後述する`group()`内に記述するとその`group()`内のテストの終了直前に実行されます。

#### `group()`

```dart

  group('apple test', skip: true, () {
    test('titleができる', () {});
    test('titleができる', () {});
    test('titleができる', () {});
  });

```

テストをグルーピングするのに使います。
主に設定したい前提条件を分けたい場合に使いますが、単に見通しが良くなるように分けておきたい場合でも使用します。
一括した設定でテストを行う場合は使用しなくても良いです。

第一引数にグループの説明を文字列で入れます。
第二引数は無名関数で`{}`内にテストを書いていきます。
なお、オプションとして引数の一つに`skip`があります。
bool値の`true`、または文字列を入れることで`flutter test`の集計対象から除外することができます。
テストをスキップしないのであればこの引数は削除して使用します。

#### `test()`

```dart

test('titleができる', () {

});

```

実際のテストを書いていきます。
第一引数にテストの説明を文字列で入れます。
第二引数は無名関数で`{}`内にテスト内容を書いていきます。

尚、`test()`関数にもオプションで`skip`が用意されています。

### 2-3. その他の詳細

今回は説明を割愛しますが、テスト用の関数として`setUpAll()`や`tearDownAll()`があります。
また、`group()`と`test()`には`skip`以外のオプションも用意されています。
詳しくはこちらの記事で解説されていますので、興味がある方はご覧になってみてください。

https://zenn.dev/ncdc/articles/flutter_unit_test

## 3. テストを書く、検証する
ここからは実際にテストを書いてみます。
テストをするということは、その対象クラスのメソッドが期待した動きや処理をおこなっているかを確かめることです。
どうやって確かめるのかをサンプルを元に見ていきましょう。
まずは以下に全体のコードを載せておきます。

```test/sample/sample_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preference_sample/applications/log/logger.dart';

void main() {
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preference_sample/applications/log/logger.dart';

void main() {
  // テスト全体で使う変数があればここで宣言
  const someBool = true;
  const someString = '文字です';
  const someInt = 1;
  const someIntList = [1, 2, 3];
  const someNull = null;

  // テスト全体で使う設定
  // main直下に配置すると、全てのテストの最初に実行される
  setUp(() {
    logger.d('main直下で宣言したsetup');
  });
  // テストが終了するごとに呼ばれる
  // StreamControllerやProviderContainerなどのリソース解放を行う
  tearDown(() {
    logger.d('main直下で宣言したtearDownを実行');
  });

  group('sample testその１', skip: 'サンプルのためカバレッジから除外', () {
    setUp(() {
      logger.d('group直下で宣言したsetup');
    });

    int? testMethod({required bool? flag}) {
      return switch (flag) {
        true => 1,
        false => 2,
        null => null,
      };
    }

    tearDown(() {
      logger.d('main直下で宣言したtearDownを実行');
    });

    test('マッチャーを使ったテスト', () {
      // 第一引数に検査対象、第二引数に期待値を入れる
      // 期待値をMatcher型で定義されたものから選べる
      expect(someBool, isTrue);
      expect(someString, isNotEmpty);
      expect(someInt, isNonZero);
      expect(someIntList, isList);
      expect(someNull, isNull);
    });
    test('期待値を直接入れる場合', () {
      expect(someBool, true);
      expect(someString, '文字です');
      expect(someInt, 1);
    });
    test('比較対象の値を少し編集する場合', () {
      expect(someInt < 2, isTrue);
      expect(someIntList.length, 3);
    });
    test('少し複雑な期待値をマッチャーで表現する', () {
      expect(someInt, lessThan(2));
      expect(someIntList, hasLength(3));
    });
    test('メソッドをテストする', () {
      int? result;

      result = testMethod(flag: true);
      expect(result, 1);

      result = testMethod(flag: false);
      expect(result, 2);

      result = testMethod(flag: null);
      expect(result, isNull);
    });
  });
  group('sample testその２', skip: false, () {
    setUp(() {
      logger.d('二つ目のgroup直下で宣言したsetup');
    });
    // 引数のskipに値を入れるとflutter testコマンドでのtestにスキップされる
    // このフォルダ内でテストの実行はできる
    // カバレッジにまだ含めたくない場合などで使うといい。
    test('スキップされるテスト１', skip: '今回のリリースには関係ないのでスキップ', () {
      expect(someBool, isTrue);
    });
    // falseにするとスキップされない <= 引数自体消す
    test('スキップされるテスト２', skip: true, () {
      expect(someBool, isTrue);
    });
  });
}

```
### 3-1. `except()`で確認する
```dart

 test('マッチャーを使ったテスト', () {
      // 第一引数に検査対象、第二引数に期待値を入れる
      // 期待値をMatcher型で定義されたものから選べる
      expect(someBool, isTrue);
      expect(someString, isNotEmpty);
      expect(someInt, isNonZero);
      expect(someIntList, isList);
      expect(someNull, isNull);
    });

```

expectを和訳すると「期待する」となります。
`expect()`関数では第一引数に検査対象の値を、第二引数に**検査対象の値に期待する値**を入れます。
`expect()`の定義を見ると第二引数は`dynamic matcher`でなんでも入れられるようになっています。

![スクリーンショット 2024-06-22 21.54.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/7d18191b-57f7-604a-2718-16b1a3c554b2.png)

上記の例ではそれぞれに`Matcher`型の値を入れています。

一つ例をとってみると、`expect(someBool, isTrue);`が検証している内容は`someBool`という値は`true`である、ということです。

ここでこのテストブロックの左上にある`Run`を押すとこのテストだけを実行して結果を確認することができます。
テストブロック内の全ての条件があっている場合に左の緑のチェックマークがつきます。
つまりテスト成功です👍🏻

![スクリーンショット 2024-06-22 22.00.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/1eb08c6e-117a-08ff-41fc-da3b4474b3f2.png)

逆にこのテストブロック内の`expect()`が一つでもあっていなければテストは失敗します。
試しに`expect(someBool, isFalse);で実行してみると以下のようになります。

![スクリーンショット 2024-06-22 22.06.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/7a00f6e7-407f-0dcf-0003-0e98f40e4472.png)

今回は本来テストでは使わないloggerでログを出しているので少々見づらいのですが、テストの横に赤いバッテンマークがついてテストが失敗したことを明示しています。

さらにスクショ上部の赤枠や画面下の「テスト結果」タブにも表示がありますが、どこのテストがなぜ失敗したかが確認できます。

Expected: false => falseになることを期待したけど
 Actual: <true> => 結果はtrueだった
 test/sample/sample_test.dart 43:7  => 問題の場所は４３行目のコード

:::note
`main()`の上の'Run'を押すと全てのテストが実行されます。
`group()` の上の'Run'を押すとそのグループ内のテストが実行されます。
:::

### 3-2. `Matcher`は沢山ある

https://api.flutter.dev/flutter/package-matcher_matcher/package-matcher_matcher-library.html

ものすごく沢山あります。
覚えるのは正直しんどいので、よく使うものは覚えてそれ以外は「あれ、これあるかなぁ？」と思った時に上記の公式ページで確認するようにすればいいと思います。

少し複雑で使いそうなものとして以下のものでしょうか。

```dart

    test('少し複雑な期待値をマッチャーで表現する', () {
      expect(someInt, lessThan(2));
      expect(someIntList, hasLength(3));
    });

```
`lessThan(2)` => ２より小さい
`hasLength(3)` => 要素が３こある

### 3-3. そもそも`Matcher`を使わない

チャブ台を返すようですが、`Matcher`型の値を使わずに直接値を指定する、または検証対象の値を少し編集すれば同じことは表現できます。
なぜなら先ほども紹介しましたが、第二引数は`dynamic matcher`なので、なんでも入れられるからです。

```dart

    test('期待値を直接入れる場合', () {
      expect(someBool, true);
      expect(someString, '文字です');
      expect(someInt, 1);
    });

    test('比較対象の値を少し編集する場合', () {
      expect(someInt < 2, isTrue);
      expect(someIntList.length, 3);
    });
    
```

`Matcher`型の値を使った方が可読性としてはいいのですが、そこで時間を使って悩むようであればひとまず値を入れてしまっても良いと思います。

### 3-4. メソッドのテストをしてみよう
```dart

    int? testMethod({required bool? flag}) {
      return switch (flag) {
        true => 1,
        false => 2,
        null => null,
      };
    }
    
```

本来は実装されたクラスのメソッドを使いますが、今回はテストコード内で定義したメソッドを検証してみます。
この`testMethod`は引数に`bool?`を受け取ります。
引数の値によって`int?`を返却するという実装です。

```dart

    test('メソッドをテストする', () {
      int? result;

      result = testMethod(flag: true);
      expect(result, 1);

      result = testMethod(flag: false);
      expect(result, 2);

      result = testMethod(flag: null);
      expect(result, isNull);
    });

```

最初にメソッドの結果を受け取る変数を作ります。
そして引数に３通りの値を入れて実行した結果を変数で受け取り、それぞれを検証しています。

### 3-4. `skip`の挙動
```dart

  group('sample testその１', skip: 'サンプルのためカバレッジから除外', () {

  // 省略

  });

  group('sample testその２', skip: false, () {
    setUp(() {
      logger.d('二つ目のgroup直下で宣言したsetup');
    });
    // 引数のskipに値を入れるとflutter testコマンドでのtestにスキップされる
    // このフォルダ内でテストの実行はできる
    // カバレッジにまだ含めたくない場合などで使うといい。
    test('スキップされるテスト１', skip: '今回のリリースには関係ないのでスキップ', () {
      expect(someBool, isTrue);
    });
    // falseにするとスキップされない <= 引数自体消す
    test('スキップされるテスト２', skip: true, () {
      expect(someBool, isTrue);
    });
  });
  
```

`group()`や`test()`のオプションである`skip`についてです。

今回のテストはテストを実行して検証することは可能です。
しかし先にも説明した`flutter test`コマンドを実行すると検証結果からは除外されます。

![スクリーンショット 2024-06-22 22.47.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/22657459-af24-6410-0ca8-bd8f3b043113.png)

個人的には文字列でなぜこのテストをスキップするのか理由を書いた方がわかりやすいと思いました。
bool値を使う場合はテストを書いている最中で、一時的にテストをオフにしておきたい時だけかと思いました。

### 余談：`setUp()`と`tearDown()`の実行タイミング

![スクリーンショット 2024-06-22 22.58.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/a32e0a1f-825a-04d5-a73b-9931a4510e62.png)

このテストファイルの`main()`関数上の`Run`を実行してログを確認してみてください。
すると`mian()`関数直下の`setUp()`と`tearDown()`、
`group()`内の`setUp()`と`tearDown()`の実行タイミングが把握できると思います。
この挙動を参考に、今後はどこでどの関数を使うべきかを判断するといいでしょう。

ログはデバックコンソールかテスト結果のタブ内で確認できます。

:::note 
基本方針としては
`mian()`関数直下に`setUp()`と`tearDown()`を置いて、全てのテストケースに適用するのが無難だと思います。
:::

:::note
前提条件がテストケースによって違う場合
`mian()`関数直下には最低限の内容で`setUp()`と`tearDown()`を記述
`group()`ごとにそれぞれ必要な設定を`setUp()`と`tearDown()`を記述

などが考えられます。
:::

## 終わりに

いかがでしたでしょうか。
この記事ではFlutterにおける単体テストの基礎をサンプルを使って解説しました。
テストはエンジニアにとって非常に重要なスキルですので、少しずつ慣れていきたいところです。

正直、実装よりも楽しくない**作業**となりがちなためあまり気が進まない方は多いと思います。
自分はそうでした🙂
しかし、自身の書いたコードが仕様に沿ったものなのか確認し、コードの品質を担保する作業でもあります。

今回は基礎編ということでしたが、次回は実際の実装を使ってさらに具体的な内容を解説できればと思っています。

この記事がFlutter学習者のお役に立てれば幸いです。

## 実際の実装を使ってユニットテストを書いた記事

https://qiita.com/Harx02663971/items/53364b9dadb46f381771