Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Flutter のテストをちゃんと書いてみる 〜ユニットテスト編〜

More than 1 year has passed since last update.

Flutter アプリ開発を初めてはや半年近く、正直に言ってテストコードなんて1行も書けていなかったので、ある程度アプリ開発の流れがわかったこのタイミングでちゃんと調べてみました。

Flutterのテストについて調べた | @yujikawa

こちらの記事に記載の通り、Flutter にはユニットテスト(Unit tests)、ウィジェットテスト(Widget tests)、インテグレーションテスト(Integration tests)が用意されているため、この記事ではまずユニットテストを書くところから始めたいと思います。

参考

この記事を書くにあたり、主に以下の動画、ページを参考にしました。

The Boring Flutter Development Show, Ep. 21 (Youtube)

Testing Flutter Apps - Making Sure Your Code Works (The Boring Flutter Development Show, Ep. 21)

Google の開発者があれこれ雑談しながらノーカットで Flutter アプリを開発する様を垂れ流す "The Boring Flutter Development Show" のテストコード回です。

Flutter 特有のテストの書き方や考え方に加え、どの程度の粒度でテストを書けば良いのか、テスト対象の可視性が private だった場合にどうすれば良いのか、など、テストコードを書く際のよくある疑問にも触れているため、 「テストコードを書く」という行為自体に慣れていない(僕のような)人でも分かりやすく勉強になります

Flutter 公式ドキュメント

Testing Flutter apps - Flutter

Flutter 公式ドキュメントにある、テストに関するページです。
体系立てて調べたい場合はこちらを端から読んでいくと良いと思います。この記事も基本的にはこちらを読みながら進めていきます。

本文

テスト用のアプリを用意する

ただテストパッケージで使えるメソッドを試していくだけでは楽しくないため、下のような 1 ページだけの簡単なアプリをテスト駆動っぽい進め方で作っていきたいと思います。

IMG_0379.jpg

記事一覧アプリの想定で、主な画面イメージと仕様は落書きに記載の通りです。テストコードを書きやすいよう、一部現実ではあまり見ないような仕様も入れています。(最後に「いいね」された日付を出す、とか)

テスト対象になるクラスとメソッドのスケルトンを作成する

まずは、このアプリで登場するクラスを作成し、使うであろうメソッドをいくつか用意します。中身の実装はまだです。

lib/model/article.dart ... 記事データを表すクラス
class Article {
  String title;
  DateTime lastUpdated;
  bool isLiked;
  int likeCount;

  Article(
    this.title,
    this.lastUpdated,
    this.isLiked,
    this.likeCount,
  );
}

lib/model/article_list.dart ... 記事一覧データを管理するクラス
import 'package:testing_practice/model/article.dart';

class ArticleList {
  final List<Article> list;

  const ArticleList(this.list);

  List<Article> get dataSource {
    return list; // TODO: sort by "like"
  }

  int get totalLikeCount {
    return -1; // TODO: count total "like"
  }

  void add(Article article) {
    // TODO: add an article to list
  }
}

※ この記事ではテストコード以外のコードは折りたたんでいますので、必要に応じてクラス名左の三角アイコンをクリックして開いてください。

article.dart はタイトルや「いいね」の数など、記事データ1件分を表すデータクラスです。特にロジックは含まれないため、テスト対象外です。

article_list.dart は画面に表示する記事データのリストを管理します。追加や並び替え、「いいね」数のカウントなどもここでやります。つまり、今回はこのクラスをテストします。getter やメソッドも機能に合わせて、 get dataSourceget totalLikeCountadd を用意しています。

flutter_test パッケージを追加する

Flutter アプリのテストには、test もしくは flutter_test パッケージを利用します。

Flutter プロジェクトを作成すると、デフォルトで pubspec.yaml に記載されているのは flutter_test の方で、これが Dart のユニットテスト用パッケージである test パッケージを含み、Flutter 独自の Widget をテストするための仕組みが追加されたパッケージになっているため、通常はこれをそのまま使えば問題ないでしょう。

pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter

テストコードを書く

テストコードを置くフォルダは /test 以下です。ここにテストの内容やソース管理として適切な単位でファイルを作っていきます。
今回は(おそらく)よくあるパターンに倣って、 /lib/model/article_list.dart に対するテストとして /test/model/article_list_test.dart を作成します。

作成したらまずやることは flutter_test.dart のインポートと main() のコーディングです。

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

void main() {
}

なお、このあたりは参考で紹介した "The Boring Flutter Development Show, Ep. 21" の 17:45 あたりで同じことをやっています。併せて観てみてください。

次に、テストで利用するダミーデータを setUp() で用意します。 setUp() に渡した関数が一つひとつのテストケースが実行される直前に毎回呼ばれる仕組みです。

article_list_test.dart
void main() {
  ArticleList articleListForText;
  setUp(() {
    articleListForText = ArticleList(<Article>[
      Article('Flutter today', DateTime(2019, 4, 23), false, 15),
      Article('Master of Dart', DateTime(2018, 3, 13), false, 4),
      Article('10 most popular Softwares built with Flutter', DateTime(2018, 11, 4), false, 93),
      Article('The awesome tricks you must know!', DateTime(2017, 8, 1), false, 23),
      Article('Have you tried FlutterHub?', DateTime(2019, 10, 11), true, 5),
      Article('Discussion - Provider vs Redux', DateTime(2019, 8, 3), false,31),
    ]);
  });
}

次に、 group()test() 、そして expect() でテストケースを追加していきます。

article_list_test.dart
  group('dataSource test', () {
    test("dataSource has 6 data, same as initial data count", () {
      expect(articleListForText.dataSource.length, 6);
    });

    test("dataSource has 7 data count in total after adding one data", () {
      articleListForText.add(Article("Flutter ver 1.10", DateTime(2019, 12, 12), false, 0));
      expect(articleListForText.dataSource.length, 7);
    });
  });

group() はテストケースをグループ化してテストケースの塊を分かりやすくするための関数です。
test() はテストケース1件を表す関数です。

group()test() とも第1引数にグループやテストのタイトルを渡します。ここではグループ名に 「(Widgetが参照する想定の) dataSource に関するテスト」 を、各テストケースとして get dataSource を通しても件数が変わらず6件であること」 「1件追加したら合計7件になること」 の2つを追加しました。

test() の第2引数に関数でテストの内容を書いていきます。 expect() 関数を使って、第1引数と第2引数に渡した値が等しいかどうかをテストします。

expect(articleListForText.dataSource.length, 6);

このように書けば articleListForText.dataSource.length6 かどうかをテストして、等しければパス、違っていれば失敗となる、となります。

articleListForText.add(Article("Flutter ver 1.10", DateTime(2019, 12, 12), false, 0));
expect(articleListForText.dataSource.length, 7);

2つ目のテストは expect() 関数の前に add()Article を1件追加したあとに件数が7件かどうかをテストしています。
コードからも、「初期状態6件に1件追加したので合計が7件になっているはず」というテストの意図が分かりやすいですね。

テストを実行する

さて、テストケースが2つできたので、試しに実行してみたいと思います。

Flutter では、VSCode の拡張機能を使って、以下の "Run" ボタンからテストケースごと、グループごとに実行できます。

image.png

グループの方の "Run" ボタンをクリックして実行すると、結果が左側に、詳細なメッセージが右下のデバッグコンソールに表示されます。

image.png

今回は1つ目のテストケースはパス、2つ目のテストケースは失敗というのが分かりますね。失敗の原因も「7件の予想が実際には6件だった」とのことで、これはまだ add() メソッドを何も実装していないので想定内です。

1件目が成功しているのは、とりあえず get dataSource でそのまま保持しているリストを返却しているので、件数だけ見たら OK だったということですね。ただし「いいね」数順のソートをしていないため、この後並び順に関するテストを書いたら失敗すると思います。

なお、プロジェクト全体のテストを実行したい場合は、ターミナルで flutter test を実行します。

$ flutter test
00:01 +1 -1: dataSource test dataSource has 7 data count in total after adding one data [E]                 
  Expected: <7>
    Actual: <6>

  package:test_api                                   expect
  package:flutter_test/src/widget_tester.dart 229:3  expect
  model/article_list_test.dart 26:7                  main.<fn>.<fn>

00:01 +1 -1: Some tests failed.                        

残りのテストケースを書く

同じ要領で「いいね」数のカウントや並べ替えについてもテストを書いていきます。

article_list_test.dart
  group('order of dataSource', () {
    test('the first data is "10 most popular Softwares built with Flutter"', () {
      expect(
        articleListForText.dataSource.first.title,
        '10 most popular Softwares built with Flutter'
      );
    });

    test('the last data is "Master of Dart"', () {
      expect(
        articleListForText.dataSource.last.title,
        'Master of Dart'
      );
    });
  });

  group('total like count', () {
    test('total like count is 171', () {
      expect(articleListForText.totalLikeCount, 171);
    });
  });

ソート後の最初の記事、最後の記事のタイトルが想定通りかのテストや、トータルの「いいね」数がちゃんと 171(電卓で計算しました) になっているかどうかのテストを追加しています。

追加したらまたテストを実行し予想通り失敗することを確認します。(結果の添付は省略します)

機能を実装する

さて、テストが書けたら(本当はまだまだ少ないですが、、)いよいよ機能の実装です。

lib/model/article_list.dart の TODO 部分に並べ替え、「いいね」数のカウント、記事の追加のロジックを書いていきます。

各機能を実装した article_list.dart
class ArticleList {
  final List<Article> list;

  const ArticleList(this.list);

  List<Article> get dataSource {
    list.sort((article1, article2) => article2.likeCount - article1.likeCount);
    return list;
  }

  int get totalLikeCount {
    return list.fold(0, (current, next) => current + next.likeCount);
  }

  void add(Article article) {
    list.add(article);
  }
}

実装したら、改めてテストを実行して不具合がないかを調べます。

$ flutter test
00:01 +5: All tests passed! 

良いですね。5つ全てのテストがパスしました。

実装に何か不備があればここでテストが失敗して教えてくれます。実際、僕が最初にテキトーにコードを書いたら dataSource の並べ替えが逆になっていたため、以下のようにテストが失敗していました。

$ flutter test
00:01 +2 -1: order of dataSource the first data is "10 most popular Softwares built with Flutter" [E]       
  Expected: '10 most popular Softwares built with Flutter'
    Actual: 'Master of Dart'
     Which: is different.
            Expected: 10 most po ...
              Actual: Master of  ...
                      ^
             Differ at offset 0

  package:test_api                                   expect
  package:flutter_test/src/widget_tester.dart 229:3  expect
  model/article_list_test.dart 32:7                  main.<fn>.<fn>

00:01 +2 -2: order of dataSource the last data is "Master of Dart" [E]                                      
  Expected: 'Master of Dart'
    Actual: '10 most popular Softwares built with Flutter'
     Which: is different.
            Expected: Master of  ...
              Actual: 10 most po ...
                      ^
             Differ at offset 0

  package:test_api                                   expect
  package:flutter_test/src/widget_tester.dart 229:3  expect
  model/article_list_test.dart 39:7                  main.<fn>.<fn>

00:01 +3 -2: Some tests failed.                          

見てみると、並び順に関するそれぞれのテストケースの本のタイトルが期待と実際で逆になっていることがなんとなく書かれており、「あ、ソートが逆になっちゃってるんだな」ということにすぐに気づけました。便利です。

まとめ

以上、この記事では Flutter プロジェクトの ユニットテスト の部分をやってみました。

今までは「テスト駆動開発ってやった方がいいんだろうけど実際やったら手間暇かかるんだろうなー」という月並みな認識を持っていたのですが、今回ちょっとしたテスト駆動開発を実験してみて、確かに実装(と今後の変更)に対する安心感が段違いであることをこれだけでも感じられました。

テストケースごと、グループごとの実行も簡単にできるため、実装した内容をいちいちアプリを起動して動作確認する必要なく素早くデバッグできるのもアプリ開発におけるテストコードの魅力だと思います。

次回は Widget テストに挑戦してみたいと思います。お楽しみに。


この記事で使っているプロジェクトは GitHub にアップしてあります。必要に応じてご覧ください。

chooyan-eng/FlutterTestPractice - GitHub

chooyan_eng
フリーランスでアプリ開発や講師をやってます。Flutter, ネイティブでの iOSアプリ開発、Androidアプリ開発が最近のメインです。 https://zenn.dev/chooyan の方でも Flutter の仕組みを調べて説明する記事を書いています。開発、研修のお仕事はご相談ください。
https://tsuyoshichujo.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away