Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

Flutterのテストって一体どんな風にやるんだろう?もしくは何ができるんだろうと思ったので、テストについて色々と調べたことを書きたいと思います。

テストの種類

Flutterにおいて、テストは3種類あります。
ユニットテスト、ウィジェットテスト、インテグレーションテストがあります。各テストは下記のような内訳になります。

ユニット ウィジェット インテグレーション
信頼度 最高
メンテナンスコスト 最高
依存度 めっちゃ多い
実行速度 速い 遅め めっちゃ遅い

ユニットテスト

testパッケージを導入します。

pubspec.yaml
dev_dependencies:
  flutter_test:
    sdk: flutter
  test: ^1.5.1

簡単なテストを書いてみる

test/unit_test.dart
import 'package:test/test.dart';

void main() {
  test('my first unit test', (){
    var answer = 2;
    expect(answer, 2);
  });
}

テストを走らせるコマンドを入力すると動きます。

テスト成功
~$ flutter test
00:03 +2: All tests passed! 
テスト失敗
~$ flutter test
00:02 +0 -1: /Users/kawakami/study/flutter/sample/test/unit_test.dart: my first unit test [E]                                                                                                   
  Expected: <5>
    Actual: <2>

  package:test_api    expect
  unit_test.dart 6:5  main.<fn>

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

単一ファイルを動かすときは下記のようにします。

~$ flutter test test/unit_test.dart

ウィジェットテスト

Flutterプロジェクトを立ち上げた時にデフォルトでカウントアップのプログラムのテストコードが書いているので、それをベースに見てみます。デフォルトプログラムの実行結果が下図です。
test.gif

test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:sample/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });

}

まずユニットテストの違いでいうとflutter用のテストパッケージを利用します。

import 'package:flutter_test/flutter_test.dart';
testWidgets('Counter increments smoke test', (WidgetTester tester) 

testWidgets関数を使い、第二引数にWigetTester引数の関数を定義するようになっています。

await tester.pumpWidget(MyApp());

ここのpumpWidgetでアプリの立ち上げをやります。
そのあとは実際に画面が立ち上がっているイメージで操作をして変化をテストできます。

    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

この部分で起動時の状態のテストを行なっています。findsOneWidgetやfindsNothingは用意されているオブジェクトです。findsOneWidgetは期待されるウィジェットが1つ探すやつで、findsNothingは期待されるウィジェットが一つもないという状態をテストします。

    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

ここでは実際にボタンをtapしてカウントさせるアクションをしています。その後変化を起こすためにtester.pumpで状態変更します。

    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);

最後は1にカウントされた状態のテストが書かれています。

インテグレーションテスト

エミュレータを使ってテストコードが実行されます!
flutter driverというパッケージを使います。
まずはパッケージの導入です

pubspec.yml
dev_dependencies:
  flutter_driver:
    sdk: flutter

テストファイルはtest_driverというフォルダを作成してそちらに作成していきます
ファイル名はapp.dartとその名前にtestをつけたapp_test.dartとします。
appの部分はそれぞれ任意で変更して構いません。app.dartはapp
test.dartを動かすようになっていますので、app部分の名前は一致させておく必要があります。

lib
res
test
test_driver
|____app.dart
|____app_test.dart

インテグレーションテストですべきことは
①flutter driver機能をONにする
②メインアプリを起動する
③テストコードを書く
まず①と②を動作をするコードをtest_driver/app.dartに書いていきます。

test_driver/app.dart
import 'package:flutter_driver/driver_extension.dart';
import 'package:sample/main.dart' as app;

void main() {
  enableFlutterDriverExtension();
  app.main();
}

次に③の実際のテストコードを書きます。
テストの内容は「カウントアップするアプリのボタンを押したら0から1になる」というテストです。
ただテストを書く前に、表示されるテキストのウィジェットや加算するためのボタンなどの操作をどうすればいいのでしょうか?どうやって指定したらいいのでしょうか?
それを解決するためにはアプリ側のウィジェットにkeyプロパティがあるのでそれで特定のウィジェットを指定できるようにします。

下記のコードのTextウィジェットとFloatingActionButtonウィジェットにkeyプロパティを設定しています。Keyクラスの引数に名前を入れるとその名前でアクセスできるようになります(わかりやすい!)

lib/main.dart
/// 省略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
              key: Key('counter'), // 追加
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: Key('increment'), // 追加
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

ではテストコードを書いていきます。

test_driver/app_test.dart
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {

    final counterTextFinder = find.byValueKey('counter');
    final buttonFinder = find.byValueKey('increment');

    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('starts at 0', () async {
      expect(await driver.getText(counterTextFinder), "0");
    });

    test('increments the counter', () async {
      await driver.tap(buttonFinder);
      expect(await driver.getText(counterTextFinder), "1");
    });
  });
}

setAppAll関数はテスト実行前に動作するもので主に準備用に使っています。ここではFlutterDriverにコネクトする処理が書いてあります。
tearDownAllはテスト終了時に実行されるものです。setAppAllで接続した状態をクローズする処理が書いてあります。

あとはtest関数を用いてテストを書いていくだけです。
driver.getTextで指定したウィジェットのテキストが取れます。今回加算されているウィジェットを指定しています。

実行
~$ flutter drive --target=test_driver/app.dart
Using device iPhone XR.
Starting application: test_driver/app.dart
Starting Xcode build...                                          
 ├─Assembling Flutter resources...                    2.2s                                                                                                                                       
 └─Compiling, linking and signing...                  2.7s                                                                                                                                       
Xcode build done.                                            6.9s                                                                                                                            4.4s
flutter: Observatory listening on http://127.0.0.1:55309/
00:00 +0: Counter App (setUpAll)
[info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:55309/
[trace] FlutterDriver: Isolate found with number: 985713800
[trace] FlutterDriver: Isolate is paused at start.
[trace] FlutterDriver: Attempting to resume isolate
[trace] FlutterDriver: Waiting for service extension
[info ] FlutterDriver: Connected to Flutter application.
00:01 +0: Counter App starts at 0
00:01 +1: Counter App increments the counter
00:01 +2: Counter App (tearDownAll)
00:01 +2: All tests passed!
Stopping application instance.

実際に実行すると下図のようにエミュレータ上でテストコードが実行されます!すごく地味ですがテストコードによりボタンが押されて加算されている状態がわかります。

drive1.gif

おわりに

Flutterのテストに触れてみましたが導入もとても楽でわかりやすくてとても書いていて楽しかったです。

ソースコード

https://github.com/yujikawa/flutter_test_sample

Why do not you register as a user and use Qiita more conveniently?
  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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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