單元測試、元件測試、集成測試
單元測試(Unit Test)
用於測試單一函數、方法、類別。
首先是Flutter官網,一步一步教你如何寫單元測試。
基本判斷式:
// 判斷是否相等
expect(actual,matcher);// 判斷是否不為空
expect(actual, isNotNull);// 同expect,但不檢查尚未完成的異步API請求
expectSync();// 同expect,但在符合時回傳一個Future
expectLater();
matcher:
expect("foo,bar,baz", allOf([
contains("foo"),
isNot(startsWith("bar")),
endsWith("baz")
]));
相關的測試放在一個group:
group('testSomeFunc', () { test('test1', () { expect(width, 50); }); test('test2', () { expect(height, 100); });}
初始設定:
每次測試運行前的初始函數,在測試案例中只被調用一次。
可以異步運行,且返回Future。
void main() { MovieList movieList; setUpAll(() { // 這個檔案中的所有測試運行前 }); setUp(() { // 每個測試案例運行前 print('setup 1'); movieList = new MovieList(); }); tearDownAll(() { // 這個檔案中的所有測試結束後
print('tearDown'); }); group("group test 1", () { setUp(() { // group 裡面的每個測試案例運行前
print('setup 2'); });
test('test 1', () {
}); });}
這樣一來每個case 都會進入setup 1、setup 2,適用於初始資料和狀態,使每個test case 都是新的開始,測試資料的乾淨度會影響測試結果。
terminal執行:
$flutter test test/name_test.dart
或 VSCode 選擇 Debug / Start Debugging
然後重點來了!測試的精髓在於你寫了什麼?
單元測試的核心概念很簡單,就是你的input和output是否符合期望值。
↑ 裡面有等價類劃分和邊界值分析
寫測試就等於給你的程式上一個石膏模,當你程式該有的形狀被改壞時,石膏模就會破掉,提醒你要重新檢查程式和測試。
— — — — — (以下進入小劇場模式) — — — — —
某天收到一個issue說要新增一個計算BMI的功能,然後很快就生出code。
double getBMI(double hieght, double weight) { return weight / (hieght * hieght);}
嗯……寫是寫完了,但出來結果對不對咧?寫個測試案例try try看。
group('test getBMI', () { test('check result', () { TestBMI bmi = TestBMI(); expect(bmi.getBMI(155, 52), 21.6); });});
然後就收到紅字條……
很明顯就是單位錯惹!立馬改!
double getBMI(double hieght, double weight) {
//身高單位轉為公尺
double hieghtUnitM = hieght / 100;
double result = weight / (hieghtUnitM * hieghtUnitM);
//四捨五入取小數點後一位,不足補0
return num.parse(result.toStringAsFixed(1));
}
再Try一次,看到綠色勾勾就安心了~
這是一般開發時都會做的事,測試input和output是否符合期待,但差別在於開發完成後不一定會留下紀錄。現在功能好像完成了,但還沒有足夠的信心說完全沒問題,那就多幾個測試吧!
發揮你內心的邪惡力量,想想如果要把這支程式搞壞,有哪些方法?
//TODO: 身高體重等於0
//TODO: 身高體重為負值
//TODO: 身高單位錯誤
然後經過邊界值分析和等價類劃分之後,就能產出測試案例。
test('正常的輸入', () { TestBMI bmi = TestBMI(); expect(bmi.getBMI(155, 52), 21.6); expect(bmi.getBMI(160, 60), 23.4); expect(bmi.getBMI(1, 1), 10000.0);});test('身高體重為0', () { TestBMI bmi = TestBMI(); expect(bmi.getBMI(0, 0), 0); expect(bmi.getBMI(1, 0), 0); expect(bmi.getBMI(0, 1), 0);});test('身高體重為負值', () { TestBMI bmi = TestBMI(); expect(bmi.getBMI(-150, 25), 11.1); expect(bmi.getBMI(120, -35), -24.3); expect(bmi.getBMI(-180, -75), -23.1);});test('身高單位錯誤', () { TestBMI bmi = TestBMI(); //公尺 expect(bmi.getBMI(1.5, 53), 235555.6); //毫米 expect(bmi.getBMI(1500, 53), 0.2);});
再來一張紅字條,原來(0,0)進去會是空值。
把參數為0篩選掉直接回傳0,或判斷計算結果為空值就回傳0,解決方式很多總之就是解決了!
— — — — — (小劇場模式結束) — — — — —
最後思考一下,還有哪些方式可以確保code被改壞了,測試就不會通過。
能抓出BUG的測試就是好測試。
元件測試(Widget Test)
用於測試單一元件的相關設定與動作。
基本判斷式:
//沒有找到符合元件
expect(find.text('test'), findsNothing);//找到一個符合元件
expect(find.text('test'), findsOneWidget);//找到特定數量符合元件
expect(find.text('test'), findsNWidgets(n));
取得元件:
Widget myButton = new IconButton( color: YBColor.silver, key: Key('someWidgetKey'), icon: ImageIcon(AssetImage('images/menu.png')),);
目前偏好用Key來識別唯一性,用Type來判斷數量是否正確。
//用唯一的key找
find.byKey(Key('someWidgetKey')//用類別找
find.byType(IconButton);//用圖示找
find.byIcon(icon);//用元件本身找
find.byWidget(myButton);
p.s. 有些情況key的使用是要特別注意的,可能會影響效能,詳細內容請看Flutter中key的作用。
動作:
testWidgets('測試某某功能', (WidgetTester tester) async { // 點擊
await tester.tap(find.text('click')); // 在測試環境中,狀態改變後flutter不會自動重新建構Widgets
// 因此須於模擬動作之後觸發渲染 // 觸發畫面渲染
await tester.pump(); // 在給定的時間內持續觸發畫面渲染,基本上會等待所有動畫完成
// 若時間內動畫仍在進行,將拋出超時錯誤
await tester.pumpAndSettle();}
執行方法跟單元測試一樣:
$flutter test test/name_test.dart
集成測試(Integration Test)
用於驗證測試的元件和服務是否如預期運作。運行於實機或模擬器。
以下不清楚的請直接閱讀 官網詳細教學文。
1.在pubspec.yaml新增flutter_driver:
dev_dependencies:
flutter_driver:
sdk: flutter
test: any
2.在lib同級新增 test_driver 資料夾,資料夾內新增檔案:app.dart、 app_test.dart
counter_app/
lib/
main.dart
test_driver/
app.dart
app_test.dart
3.test_driver/app.dart內容:
import 'package:flutter_driver/driver_extension.dart';
import 'package:counter_app/main.dart' as app;void main() {
//啟用FlutterDriver
enableFlutterDriverExtension(); //調用main()或runApp開始測試
app.main();
}
4.test_driver/app_test.dart內容:
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() { //分群測試
group('Test Some Process', () {
//使用find.byValueKey取得定義元件
final textFinder = 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();
}
}); //測試案例1
test('starts at 0', () async {
//取得文字內容並驗證
expect(await driver.getText(counterTextFinder), "0");
}); //測試案例2
test('increments the counter', () async {
//點擊按鈕
await driver.tap(buttonFinder);
//驗證文字內容
expect(await driver.getText(counterTextFinder), "1");
});
});
}
5.terminal執行:
$flutter drive --target=test_driver/app.dart
Unit和Widget測試不需要,但集成測試跑在模擬器或實機上,若要分辨在哪個平台運行,目前已知這方法有些問題,用iPhone實機測試但判斷成MacOS,還有待釐清……
if (Platform.isIOS) { print('Platform is iOS');} else if (Platform.isMacOS) { print('Platform is MacOS');} else if (Platform.isAndroid) { print('Platform is Android');} else if (Platform.isWindows) { print('Platform is Windows');} else if (Platform.isFuchsia) { print('Platform is Fuchsia');} else if (Platform.isLinux) { print('Platform is Linux');} else { print('other');}
持續集成服務(Continuous integration services)
CI可於推送代碼時自動運行測試,即時反饋測試結果。
官網是建議使用Fastlane,但根據過往使用Fastlane的經驗,自動發佈的功能不太用到,自動測試的功能用gitLab的CI/CD取代,加上Fastlane建置和維護成本稍高,因此暫時不考慮使用Fastlane。
覆蓋率(Code coverage)
重新計算覆蓋率,產生coverage/Icov.info檔案
$flutter test --coverage
使用Atom Icov-info查看覆蓋率:
- 安裝Atom後,用Atom安裝Icov-info。
- 用Atom開啟專案,開啟一個Dart檔案。
- 在選單上點選Packages > Lcov Info > Toggle,就會看到覆蓋率進度條。
- 覆蓋率低於75%進度條是紅色,接著橘色,最後達到100%顯示綠色。
哪裡有蓋到哪裡沒有,code上也都用紅色和綠色標得清清楚楚。
最後提醒各位,覆蓋率就跟code行數一樣參考用,並沒有很直接的關連保證100%的覆蓋率就不會有疏漏,或是沒有達到100%覆蓋率就無法抓出問題,寫測試的目的各不相同,只能說見仁見智。