---
tags: Flutter
---
# Flutter Test
近期開始使用Flutter完成了幾個APP,Flutter UI結構(聲明式UI)讓我感到滿驚豔的,另一方面類似Java的Dart語言也是很好上手,再來Google也提供相當多漂亮的UI可以讓開發者可以快速的開發出畫面豐富的APP。
但是產出好用的APP的同時,也要注意APP的品質。因此我開始注意到Flutter自動化測試相關的技術,以下將是我自己在研讀相關技術的研究紀錄,內容的範例其實也是使用Flutter doucment的範例,這裡就是依據文件中的範例嘗試自己實作的紀錄。
## Unit Test
unit test 主要是針對單一function、method或是class進行測試,以下我建立了一個名為Counter簡單的class,當作執行測試範例,內容如下:
```dart=
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
```
Counter內容只有兩個function,一個數字增加,一個則是數字減少。接著則開始建立測試的腳本。
### 一、導入dependency
首先,pubspec.yaml中在dev_dependencies底下新增unit test依賴。
```yaml=
dev_dependencies:
test: <last_version>
```
### 二、在test資料夾中新增測試腳本。
如下所示,我新增的一個名為counter_test.dart的檔案。

### 三、開始撰寫測試腳本內容。
每個測試項目在執行時可以參考以下寫法:
```dart=
test('測試名稱',(){
//要測試的內容
})
```
就如同dart執行進入點一樣,所有測試的進入點是main(),而我根據上面所建立要被測試的class,我撰寫了幾個簡單的測試如下:
```dart=
import 'package:test/test.dart';
import 'package:test_function_project/Counter.dart';
void main() {
late Counter counter;
setUpAll((){
//所有測試執行時執行
//print("run setUpAll()");
});
setUp((){
//每個測試執行前執行。
//print("run setUp()");
counter = Counter();
});
tearDown((){
//每個測試執行完後執行
//print("run tearDown()");
});
tearDownAll((){
//所有測試執行完後執行
//print("run tearDownAll()");
});
//第一項測試
test('Counter value should be 1', () {
print("run Counter value should be 1");
counter.increment();
expect(counter.value, 1); //判斷參數是否符合預期
});
//第二項測試
test('Counter value should be 0', () {
print("run Counter value should be 0");
counter.increment();
counter.decrement();
expect(counter.value, 0); //判斷參數是否符合預期
});
//第三項測試
test('Counter value should be 2', () {
print("run Counter value should be 2");
counter.increment();
counter.increment();
expect(counter.value, 2); //判斷參數是否符合預期
});
}
```
### 四、執行測試。
要執行測試可以透過兩種方式,一種就是透過Android studio執行,不過因為我們需要令其可以自動執行,所以重點還是擺在第二種方式:透過command方式執行。以下我還是簡單展示一下如何在Android studio之下執行,便於在撰寫時可以測試自己的測試腳本。

要直接透過Android studio執行的話,直接在main()旁邊有一個綠色的箭頭圖示,點擊它就可以執行所有測試項目,如果只要跑單個測試項目,在每個測試項目旁邊也會有類似的圖示,點擊該圖示即可。
接著說明一下執行測試的command,如下所示:
flutter test test/counter_test.dart
以下是執行結果:

可以看到如果所以的測試項目都順利通過,最下面會有一行ALL tests passed的說明。如果其中有錯誤的話則會顯示類似下面的訊息。

上面會說明是哪個測試名稱的項目失敗。以上就是最基本的執行測試方法,接著來說明一下如何產生測試報告。
### 五、產生報告。
很遺憾的是,flutter目前似乎沒有如同android那樣在每次測試時產出精美的網頁格式的報告,在我目前所收集到的資料裡,可以透過
--reporter
這個參數來指定要輸出的報告格式,我這裡則是選用json格式輸出。至於為什麼要使用json格式輸出,則是因為在我反覆嘗試的過程中,相較於expanded格式的報告輸出,json格式的報告所包含的內容更為詳細一點,雖然在產出後還需要透過另一支程式進行json內容解析,但我還是覺得這樣所產出的報告能提供更多的資訊。而產出json格式的報告command如下:
flutter test --reporter json test/counter_test.dart > unit_report.json
產出json報告後我們將輸出的內容全部搞入report.json檔案內,為的是可以用來程式來解析內容。要解析report的內容,我們需要另一隻可以被獨立執行的dart檔,所以在專案的lib底下新增一的名為report.dart的檔案,並在裡面新增以下內容:
```dart=
import 'dart:io';
import 'dart:convert';
import 'package:testreport/testreport.dart';
void main(List<String> args) async {
final file = File(args[0]);
final lines = LineSplitter().bind(utf8.decoder.bind(file.openRead()));
final report = await createReport(file.lastModifiedSync(), lines);
int suiteCount = report.suites.length;
// print("report.suites: $suiteCount");
for (final suite in report.suites) {
int problemCount = suite.problems.length;
if(problemCount < 1){
print("All Tests Pass");
}else{
print("problems: $problemCount");
for (final test in suite.problems) {
for (final problem in test.problems) {
print("Test Path : ${suite.path}");
print("Test Name : ${test.name}");
print("Test Message : ${problem.message}");
if(test.prints.length > 0){
print("Test Stacktrace : ");
for(final message in test.prints){
print(message);
}
}
}
}
}
}
}
Future<Report> createReport(DateTime when, Stream<String> lines) async {
var processor = Processor(timestamp: when);
await for (final line in lines) {
// print(line);
try{
var jsonResult = json.decode(line) as Map<String, dynamic>;
// processor.process(json.decode(line) as Map<String, dynamic>);
processor.process(jsonResult);
}on FormatException catch(e){
// print(e.message);
}
}
return processor.report;
}
```
該程式碼引用如下:
```yaml=
dependencies:
testreport: ^2.0.1
```
完成後就可以透過以下指令去解讀json report。
dart lib/report.dart unit_report.json > report.txt
## Integration Test
延續上面的unit test,一個完整的測試除了unit test,還需要進行整合測試,跟unit test不同的事是,整合測試就需要跑模擬器或實機才能玩成。
### 一、導入dependency
首先一樣在pubspec.yaml中新增所需的依賴。
```yaml=
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
```
### 二、在integration_test資料夾中新增實行測試的dart檔
如下所示,但這個檔案似乎是在創建時就會自動新增,因此為了方便我就直接在裡面進行腳本的編輯。

### 三、撰寫測試腳本
撰寫測試腳本之前,我進行一些準備工作來執行測試。
#### 建立測試APP
為了要嘗試UI測試,因此我需要見一個用才測試的APP,以下是我建立用來測試的APP

分別有一個增加數值以及減少數值的按鈕,中間則有顯示數值的文字元件,再來就是切換到第二頁以及切換的裡面有兩個ListView元件的畫面。
#### 基本判斷式
先介紹一下測試單一元件基本的判斷式:
```dart=
//沒有找到符合元件
expect(find.text('test'), findsNothing);
//找到一個符合元件
expect(find.text('test'), findsOneWidget);
//找到特定數量符合元件
expect(find.text('test'), findsNWidgets(n));
```
#### 找到特定元件
在UI測試中,需要能夠找到特定的元件,並對其進行動作或是內容驗證的行為。目前發現可以在建UI畫面的時候可以給予元件一個唯一的Key值,這個內容可以自己給定,在測試時再根據key找到對應的元件。例如以下範例:
在建置Button時給予key值如下:
```dart=
MaterialButton(
key: const Key("button_increment"),
child: const Text("incrementButton"),
onPressed: _incrementCounter),
```
而在測試腳本中就可以透過以下方式找到這個button:
```dart=
final Finder finderIncrementButton = find.byKey(const Key('button_increment'));
await tester.tap(finderIncrementButton); //點擊按鈕
```
以下我建立了三個測試項目:
```dart=
void main() {
//初始化IntegrationTestWidgetsFlutterBinding
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Counter Test',
(tester) async {
app.main();
await tester.pumpAndSettle(); //等待動畫結束
final Finder finderText = find.byKey(const Key("tv_counter")); //找對應元件
final Finder finderIncrementButton = find.byKey(const Key('button_increment'));
await tester.tap(finderIncrementButton); //點擊按鈕
await tester.pumpAndSettle();
Text text = tester.firstWidget(finderText);
print("text value: ${text.data}");
expect(text.data, '1'); //確認數值
final Finder finderDecrementButton = find.byKey(const Key('button_decrement'));
await tester.tap(finderDecrementButton);
await tester.pumpAndSettle();
final Text text2 = tester.firstWidget(finderText);
print("text2 value: ${text2.data}");
expect(text2.data, '0');
});
testWidgets('Change Page Test',
(tester) async {
app.main();
await tester.pumpAndSettle();
final Finder toSecondButton = find.byKey(const Key('to_second_page'));
await tester.tap(toSecondButton); //點擊切換到第二頁面的按鈕
await tester.pumpAndSettle();
expect(find.text('Second page.'), findsOneWidget); //確認頁面中是否有對應的文字元件
});
testWidgets('Scroll Test',
(tester) async {
app.main();
await tester.pumpAndSettle();
final Finder toScrollButton = find.byKey(const Key('to_scroll_page'));
await tester.tap(toScrollButton); //點擊切換到ListView的頁面按鈕
await tester.pumpAndSettle();
//在ListView中scroll到想要的位置。
await tester.dragUntilVisible(
find.byKey(const Key('item2_50_text')), // what you want to find
find.byKey(const Key('long_list2')), // widget you want to scroll
const Offset(0, -200), // move
);
});
}
```
### 四、執行測試
執行測試的command如下:
flutter test integration_test/app_test.dart
### 五、產出報告
同樣的我們除了測試以外也需要取得測試的報告,就如同unit test一樣,我是使用json格式的報告再進行解析。
flutter test --reporter json integration_test/app_test.dart > integration.json
最後一樣透過report.dart進行內容解析:
dart lib/report.dart integration.json > integtation_report.txt
在解析報告中,如果所以測試都通過的話只會在解析的報告中看到
All Tests Pass
以下則展示一下測試發生錯誤的報告解析內容:
```
problems: 1
Test Path : /Users/roypan/flutter_project/test_function_project/integration_test/app_test.dart
Test Name : Counter Test
Test Message : Test failed. See exception logs above.
The test description was: Counter Test
Test Stacktrace :
text value: 1
text2 value: 0
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: '1'
Actual: '0'
Which: is different.
Expected: 1
Actual: 0
^
Differ at offset 0
When the exception was thrown, this was the stack:
#4 main.<anonymous closure> (file:///Users/roypan/flutter_project/test_function_project/integration_test/app_test.dart:39:9)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
This was caught by the test expectation on the following line:
file:///Users/roypan/flutter_project/test_function_project/integration_test/app_test.dart line 39
The test description was:
Counter Test
════════════════════════════════════════════════════════════════════════════════════════════════════
```