Flutter 學習之路 - 測試(單元測試,Widget 測試,整合測試)
前段時間去紐約的 Google 參加 Flutter 的聚會,聽到在 Google Material Flutter 團隊的 MH Johnson 在臺上講 Flutter 的測試,想到自己該學習了哈哈哈。
一般來說,經過良好測試的應用應該有很多 unit tests 和 widget test,通過程式碼覆蓋率(code coverage)進行跟蹤,以及需要足夠的整合測試來涵蓋所有重要的使用場景。下面的表格,總結了在不同型別測試的特點,方便在選擇的時候進行權衡:
單元測試 | Widget 測試 | 整合測試 | |
---|---|---|---|
置信度 | Low | Higher | Highest |
維護成本 | Low | Higher | Highest |
依賴 | Few | More | Lots |
執行速度 | Quick | Slower | Slowest |
那麼這三個重要程度是怎麼樣呢?這個圖可以參考一下:

單元測試
參考文章(主要就是按這個學習翻譯的,英文 ok 可以直接看官網):link
測試單一功能、方法或類。例如,被測單元的外部依賴性通常被模擬出來,如package:mockito。 單元測試通常不會讀取/寫入磁碟、渲染到螢幕,也不會從執行測試的程序外部接收使用者操作。單元測試的目標是在各種條件下驗證邏輯單元的正確性。
第一步:新增test或者flutter_test依賴
在 pubspec.yaml 裡新增如下方法(嫌麻煩可以把冒號之後寫 any)
dev_dependencies: test: <latest_version> 複製程式碼
加上以後記得按一下 Packages get
第二步:建立測試檔案
目錄結構如下:(測試檔案寫在 test 檔案裡面)
flutter_road_test/ lib/ counter.dart test/ counter_test.dart 複製程式碼
第三步:建立要測試的類
建立一個要被測試的單元,這個單元可以是一個方法或一個類,下面在 lib/counter.dart 檔案中建立 Counter 類:
class Counter { int value = 0; void increment() => value++; void decrement() => value--; } 複製程式碼
第四步:為這個類寫 test
test和 expect 都來自 test 這個包:
// Import the test package and Counter class import 'package:test/test.dart'; import 'package:counter_app/counter.dart'; void main() { test('Counter value should be incremented', () { final counter = Counter(); counter.increment(); expect(counter.value, 1); }); } 複製程式碼
第五步:如果有多個測試,可以用 group
group 裡面包含了三個測試(初始狀態測試,increment 方法測試,counter.decrement 方法測試)
import 'package:test/test.dart'; import 'package:counter_app/counter.dart'; void main() { group('Counter', () { test('value should start at 0', () { expect(Counter().value, 0); }); test('value should be incremented', () { final counter = Counter(); counter.increment(); expect(counter.value, 1); }); test('value should be decremented', () { final counter = Counter(); counter.decrement(); expect(counter.value, -1); }); }); } 複製程式碼
第六步:執行測試
在命令列下執行:
flutter test test/counter_test.dart 複製程式碼
結果:

其他執行方式可以在這裡看:link
Widget 測試
參考文章(主要就是按這個學習翻譯的,英文 ok 可以直接看官網):link
第一步:新增 flutter_test 包
為什麼要用這個包,不用前面的 test 包呢,因為 flutter_test 包有下面這些功能:
- 等等再補充
dev_dependencies: flutter_test: sdk: flutter 複製程式碼
第二步:建立一個要測試的 Widget
class MyWidget extends StatelessWidget { final String title; final String message; const MyWidget({ Key key, @required this.title, @required this.message, }) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text(title), ), body: Center( child: Text(message), ), ), ); } } 複製程式碼
第三步:寫測試程式碼
有了要測試的 Widget 以後,可以開始寫測試了:
- 建立一個 testWidgets 方法
- 用 tester.pumpWidget 來建立一個 MyWidget
- 用 finder 來在 Widget tree 中找到 title 和 message 的 Text Widgets
- 用 expect 和 findsOneWidget 來測試這個 Widget 是不是隻出現了一次
void main() { // with Widgets in the test environment. testWidgets('MyWidget has a title and message', (WidgetTester tester) async { // Create the Widget tell the tester to build it await tester.pumpWidget(MyWidget(title: 'T', message: 'M')); // Create our Finders final titleFinder = find.text('T'); final messageFinder = find.text('M'); // Use the `findsOneWidget` matcher provided by flutter_test to verify our // Text Widgets appear exactly once in the Widget tree expect(titleFinder, findsOneWidget); expect(messageFinder, findsOneWidget); }); } 複製程式碼
第四步:執行測試
發現在 Android Studio 裡郵件程式碼檔案點執行就能運行了:

補充
[1] 第三步中的 WidgetTester 除了 pumpWidget 還提供了其他的方法,在使用 StatefulWidget 或者 animations 的時候可以用到:
-
tester.pump():文件連結
-
tester.pumpAndSettle():文件連結
[2] 第三步中的 findsOneWidget 是一個 Matcher ,還有一些其他的 Matcher 可以用:
findsOneWidget 只有一個對應的 Widget findsNothing 沒有找到對應的 Widget findsWidgets 找到一個或一個以上對應的 Widget findsNWidgets 找到 N 個 Widget 最後那個查了一下 API, 是這麼用的: expect(find.text('Save'), findsNWidgets(2)); 複製程式碼
整合測試
參考文章(主要就是按這個學習翻譯的,英文 ok 可以直接看官網):link
單元測試 和 Widget 測試可以用於測試單獨的 class, function, 和 Widget。當要測試各部分一起執行或者測試一個 application 在真實裝置上執行的表現的時候就要用到 整合測試 。
第一步:建立要測試的 App
首先,要建立一個被測試的 App, 這個應該功能就是按懸浮按鈕 +1,就是初始給的那個 demo, 不一樣的在於這裡我們給 Text 和 FloatingActionButton 添加了 ValueKey 以便在測試時識別這些特點的 Widgets。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Counter App', home: MyHomePage(title: 'Counter App Home Page'), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @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', // Provide a Key to this specific Text Widget. This allows us // to identify this specific Widget from inside our test suite and // read the text. key: Key('counter'), style: Theme.of(context).textTheme.display1, ), ], ), ), floatingActionButton: FloatingActionButton( // Provide a Key to this the button. This allows us to find this // specific button and tap it inside the test suite. key: Key('increment'), onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } } 複製程式碼
第二步:新增 flutter_driver 依賴
在整合測試中要用到 flutter_driver ,在 pubspec.yaml 中加入它。
dev_dependencies: flutter_driver: sdk: flutter test: any 複製程式碼
同時,也添加了 test ,因為也要用到這裡面的方法和斷言。
第三步:寫 test 程式碼
- 建立資料夾 test_driver (和 lib 檔案同級)
- 在資料夾下建立兩個檔案(命名可以隨意),一個是建立指令化的 Flutter 應用程式,使我們能 "執行" 這個app,並記錄執行的 performance (app.dart),另一個用於寫測試來判斷app 是不是按預期執行(app_test.dart),目錄如下:
flutter_road_test/ lib/ main.dart test_driver/ app.dart app_test.dart 複製程式碼
第四步:建立指令化的 Flutter 應用程式
建立指令化的 Flutter 應用程式要有這兩步:
- 它啟用了Flutter Driver的擴充套件
- 執行 App
在 test_driver/app.dart 檔案裡寫下這兩步:
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_road_test/main.dart' as app; void main() { // This line enables the extension enableFlutterDriverExtension(); // Call the `main()` function of your app or call `runApp` with any widget you // are interested in testing. app.main(); } 複製程式碼
第五步:寫測試
現在有了指令化的 app, 我們要寫測試了,測試需要下面四步:
- 用 SeralizableFinders 來定位特定的 Widgets
- 測試前,在 setUpAll 方法中連線 app
- 測試一些重要的情況
- 當測試完,在 teardownAll 方法中斷開連線
// Imports the Flutter Driver API import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; void main() { group('Counter App', () { // 通過 Finders 找到對應的 Widgets final counterTextFinder = find.byValueKey('counter'); final buttonFinder = find.byValueKey('increment'); FlutterDriver driver; // 連線 Flutter driver setUpAll(() async { driver = await FlutterDriver.connect(); }); // 當測試完成斷開連線 tearDownAll(() async { if (driver != null) { driver.close(); } }); test('starts at 0', () async { // 用 `driver.getText` 來判斷 counter 初始化是 0 expect(await driver.getText(counterTextFinder), "0"); }); test('increments the counter', () async { // 首先,點選按鈕 await driver.tap(buttonFinder); // 然後,判斷是否增加了 1 expect(await driver.getText(counterTextFinder), "1"); }); }); } 複製程式碼
第六步:執行測試
執行一個 Android 或者 iOS 模擬器或者連上自己的手機,然後從專案根目錄下執行下面的命令:
flutter drive --target=test_driver/app.dart 複製程式碼
命令的作用:
- 執行目標 app 並安裝
- 啟動 app
- 執行 app_test.dart 裡的測試
結果:
