許多我們build的Widget不僅僅顯示資訊,也響應使用者互動。包括可點選的按鈕,在螢幕拖動控制元件,或者在文字框裡輸入文字。
為了測試那些互動,我們需要一種在測試環境模擬它們的方法。為了這麼做,我們需要使用flutter_test
類庫的WidgetTester
類。
這個WidgetTester
提供了輸入文字,點選,拖拽的方法。
在很多情況下,使用者互動將會更新我們app的狀態。在測試環境裡,在狀態改變後Flutter不會自動重新構建Widgets。為了確保我們的Widget樹在我們模擬使用者互動以後重新build,我們必須呼叫WidgetTester
提供的pump
或者pumpAndSettle
方法。
步驟:
- 建立一個Widget用於測試
- 在輸入框裡輸入文字
- 確保點選按鈕會新增todo
- 確保滑動刪除todo
1. 建立一個Widget用於測試
在這個例子裡,我們將會建立一個基礎的todo app。它將會有3個需要我們測試的主要功能:
- 在
TextField
裡輸入文字 - 點選
FloatingActionButton
按鈕在todo列表裡新增文字 - 滑動從列表中刪除一個item
為了保持焦點在測試上,這個例子將不會提供詳細的構建todo app的介面。如果想學習更多關於如何構建app,請檢視以下相關文章:
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
static const _appTitle = 'Todo List';
final todos = <String>[];
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _appTitle,
home: Scaffold(
appBar: AppBar(
title: Text(_appTitle),
),
body: Column(
children: [
TextField(
controller: controller,
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
final todo = todos[index];
return Dismissible(
key: Key('$todo$index'),
onDismissed: (direction) => todos.removeAt(index),
child: ListTile(title: Text(todo)),
background: Container(color: Colors.red),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
todos.add(controller.text);
controller.clear();
});
},
child: Icon(Icons.add),
),
),
);
}
}
複製程式碼
2. 在輸入框裡輸入文字
現在我們有了一個todo app,我們可以開始寫我們的測試了!在這個示例裡,我們將會從輸入文字到TextField
開始。
我們可以這樣完成任務:
- 在測試環境build一個Widget
- 使用
WidgetTester
的enterText
方法
testWidgets('Add and remove a todo', (WidgetTester tester) async {
// Build the Widget
await tester.pumpWidget(TodoList());
// Enter 'hi' into the TextField
await tester.enterText(find.byType(TextField), 'hi');
});
複製程式碼
注意: 這段程式碼基於上一段測試的程式碼。如果想學習Widget測試的核心概念,請檢視下面的文章:
3. 確保點選按鈕會新增todo
當我們在TextField
裡輸入文字之後,我們希望點選FloatingActionButton
會新增 item 到列表裡。
這些步驟包括3步:
testWidgets('Add and remove a todo', (WidgetTester tester) async {
// Enter text code...
// Tap the add button
await tester.tap(find.byType(FloatingActionButton));
// Rebuild the Widget after the state has changed
await tester.pump();
// Expect to find the item on screen
expect(find.text('hi'), findsOneWidget);
});
複製程式碼
3. 滑動從列表中刪除一個item
最後,我們要確保在滑動刪除一條todo後可以從列表中刪除它。這將包括以下3個步驟:
- 使用
drag
方法執行滑動刪除操作。 - 使用
pumpAndSettle
方法不斷的重新build我們的Widget樹直到dismiss動畫完成。 - 確保 item 從螢幕中消失。
testWidgets('Add and remove a todo', (WidgetTester tester) async {
// Enter text and add the item...
// Swipe the item to dismiss it
await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));
// Build the Widget until the dismiss animation ends
await tester.pumpAndSettle();
// Ensure the item is no longer on screen
expect(find.text('hi'), findsNothing);
});
複製程式碼
完整程式碼:
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Add and remove a todo', (WidgetTester tester) async {
// Build the Widget
await tester.pumpWidget(TodoList());
// Enter 'hi' into the TextField
await tester.enterText(find.byType(TextField), 'hi');
// Tap the add button
await tester.tap(find.byType(FloatingActionButton));
// Rebuild the Widget with the new item
await tester.pump();
// Expect to find the item on screen
expect(find.text('hi'), findsOneWidget);
// Swipe the item to dismiss it
await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));
// Build the Widget until the dismiss animation ends
await tester.pumpAndSettle();
// Ensure the item is no longer on screen
expect(find.text('hi'), findsNothing);
});
}
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
static const _appTitle = 'Todo List';
final todos = <String>[];
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _appTitle,
home: Scaffold(
appBar: AppBar(
title: Text(_appTitle),
),
body: Column(
children: [
TextField(
controller: controller,
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
final todo = todos[index];
return Dismissible(
key: Key('$todo$index'),
onDismissed: (direction) => todos.removeAt(index),
child: ListTile(title: Text(todo)),
background: Container(color: Colors.red),
);
},
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
todos.add(controller.text);
controller.clear();
});
},
child: Icon(Icons.add),
),
),
);
}
}
複製程式碼