【Flutter】単体テストを行う方法まとめ

Flutterで単体テストをする方法についてまとめました。

テストするクラスを作成する

まずテストするためのクラスを作成します。
libディレクトリにcounter.dartを以下の内容で作成します。

class Counter
{
  var value = 0;

  void increment()
  {
    value++;
  }

  void decrement()
  {
    value--;
  }
}

単体テストを書いて実行する

次に前節のクラスの単体テストを書きます。
単体テストtestディレクトリ配下に、対象のファイル名に_testサフィックスをつけた名前で作成します。
すなわち今回はcounter_test.dartを作成します。内容は以下のとおりです。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

// メイン関数を定義
void main(){
    // test関数の第一引数にテスト名、第二引数にテストを書く
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    
    // アサーションは以下のようにexpect関数を使う
    expect(counter.value, 1);
  });
}

テストを実行するには、IntelliJの場合は下図のアイコンから実行できます。

実行アイコン

また、フォルダを右クリックしてRun ‘tests in test/…’を選択するとそのフォルダ配下のテストを全て実行することもできます。

ターミナルからも以下のように実行できます。

flutter test [ファイル or フォルダのパス]

Matcherによる判定

上記のテストはMatcherを使って下記のように、自然言語のように書くこともできます。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    
    // Matcherを使った判定
    expect(counter.value, equals(1));
  });
}

使えるMacherは以下を参照してください。

api.flutter.dev

メッセージ(reason)

失敗した時のメッセージを表示するには以下のようにします。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, equals(1), reason: "Counter should be incremented by 1");
  });
}

非同期メソッドのテスト

非同期メソッドのテストも普通にかけます。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
    // 非同期メソッドのテスト
  test("increment - can increment the value", () async {
    var counter = Counter();
    counter.increment();
    await Future.delayed(const Duration(seconds: 1));
    expect(counter.value, 1);
  });

  // setUpも非同期に
  setUp(() async {
    print("setup start");
    await Future.delayed(const Duration(seconds: 1));
    print("setup end");
  });
  
  // tearDownも非同期に
  tearDown(() async {
    print("teardown start");
    await Future.delayed(const Duration(seconds: 1));
    print("teardown end");
  });
}

グルーピング

またgroup関数でいくつかのテストをグルーピングすることもできます。
グループごとに、後述の前後処理の記述やらリトライ設定やら実行プラットフォームの設定やらができるのでそういうときに便利です。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  group("increment & decrement", () {
    test("increment - can increment the value", () {
      var counter = Counter();
      counter.increment();
      expect(counter.value, 1);
    });
    test("decrement - can decrement the value", () {
      var counter = Counter();
      counter.decrement();
      expect(counter.value, -1);
    });
  });
}

テスト前後処理

setUpAll・tearDownAll関数を書くとテストの開始前と終了後にそれぞれ一回だけ処理を行うことができます。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  });
  test("decrement - can decrement the value", () {
    var counter = Counter();
    counter.decrement();
    expect(counter.value, -1);
  });

  // テストが開始する前に一回だけ呼ばれる
  setUpAll(()  {
    print("setup");
  });

  // テストが全て終了した後に一回だけ呼ばれる
  tearDownAll(() {
    print("teardown");
  });
}

ちなみに実行中の一つのテストに対してtearDown処理をかけるaddTearDownもあります。

void main() {
  test("Foo", () {
    // このテスト終了時の処理(tearDownより前に呼ばれる)
    addTearDown(() {
      print("tearDown");
    });
  });
}

個々のテストの前後処理

ここテストごとに一回ずつ呼ばれる前後処理を作りたい場合はsetUpとtearDownを使います。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  });
  test("decrement - can decrement the value", () {
    var counter = Counter();
    counter.decrement();
    expect(counter.value, -1);
  });

  // 個々のテストが開始する前に一回ずつ呼ばれる
  setUp(() {
    print("setup");
  });

  // 個々のテストが終了した後に一回ずつ呼ばれる
  tearDown(() {
    print("teardown");
  });
}

テスト対象から除外する

基本的に使うべきじゃないとは思いますが、何かしらの理由でテスト対象から除外したい場合、ファイルの最上部にSkipアトリビュートを記述します。

// テスト対象から除外する
@Skip("Skip this test because...")

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, 1);

  });
}

以下のようにテストごとにスキップすることもできます。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, 1);

  }, skip: "Skip this test because..."); // テストをスキップする
}

リトライ回数を指定

失敗した際のリトライ回数を指定することもできます。(通常は一回で失敗扱い)

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  }, retry: 3); // リトライ回数
}

特定プラットフォームでのみ実行

以下のように書くと特定のプラットフォームでのみ実行されるテストを記述できます。

import 'package:flutter_test/flutter_test.dart';
import 'package:test_flutter/counter.dart';

void main(){
  test("increment - can increment the value", () {
    var counter = Counter();
    counter.increment();
    expect(counter.value, 1);
  }, testOn: "vm"); // Dart VMでしか実行されない
}

プラットフォームの記述は以下を参照してください。

pub.dev

タイムアウト

(Timeoutアノテーションでできるはずだけど、なぜか手元の環境だとタイムアウトしないので後でもしわかったら書きます(多分わすれます))

参考

pub.dev