--- 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的檔案。 ![](https://i.imgur.com/PmHRBkM.png) ### 三、開始撰寫測試腳本內容。 每個測試項目在執行時可以參考以下寫法: ```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之下執行,便於在撰寫時可以測試自己的測試腳本。 ![](https://i.imgur.com/0pMwLiI.png) 要直接透過Android studio執行的話,直接在main()旁邊有一個綠色的箭頭圖示,點擊它就可以執行所有測試項目,如果只要跑單個測試項目,在每個測試項目旁邊也會有類似的圖示,點擊該圖示即可。 接著說明一下執行測試的command,如下所示: flutter test test/counter_test.dart 以下是執行結果: ![](https://i.imgur.com/E3GbWU9.png) 可以看到如果所以的測試項目都順利通過,最下面會有一行ALL tests passed的說明。如果其中有錯誤的話則會顯示類似下面的訊息。 ![](https://i.imgur.com/Zo4yrLA.png) 上面會說明是哪個測試名稱的項目失敗。以上就是最基本的執行測試方法,接著來說明一下如何產生測試報告。 ### 五、產生報告。 很遺憾的是,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檔 如下所示,但這個檔案似乎是在創建時就會自動新增,因此為了方便我就直接在裡面進行腳本的編輯。 ![](https://i.imgur.com/GInHSJF.png) ### 三、撰寫測試腳本 撰寫測試腳本之前,我進行一些準備工作來執行測試。 #### 建立測試APP 為了要嘗試UI測試,因此我需要見一個用才測試的APP,以下是我建立用來測試的APP ![](https://i.imgur.com/DbPSdCE.gif) 分別有一個增加數值以及減少數值的按鈕,中間則有顯示數值的文字元件,再來就是切換到第二頁以及切換的裡面有兩個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 ════════════════════════════════════════════════════════════════════════════════════════════════════ ```