Testing Flutter apps翻譯-點選,拖動和輸入文字

o動感超人o發表於2019-04-23

許多我們build的Widget不僅僅顯示資訊,也響應使用者互動。包括可點選的按鈕,在螢幕拖動控制元件,或者在文字框裡輸入文字。

為了測試那些互動,我們需要一種在測試環境模擬它們的方法。為了這麼做,我們需要使用flutter_test類庫的WidgetTester類。

這個WidgetTester提供了輸入文字,點選,拖拽的方法。

在很多情況下,使用者互動將會更新我們app的狀態。在測試環境裡,在狀態改變後Flutter不會自動重新構建Widgets。為了確保我們的Widget樹在我們模擬使用者互動以後重新build,我們必須呼叫WidgetTester提供的pump或者pumpAndSettle方法。

步驟:

  1. 建立一個Widget用於測試
  2. 在輸入框裡輸入文字
  3. 確保點選按鈕會新增todo
  4. 確保滑動刪除todo

1. 建立一個Widget用於測試

在這個例子裡,我們將會建立一個基礎的todo app。它將會有3個需要我們測試的主要功能:

  1. TextField裡輸入文字
  2. 點選 FloatingActionButton 按鈕在todo列表裡新增文字
  3. 滑動從列表中刪除一個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
  • 使用WidgetTesterenterText 方法
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步:

  1. 使用 tap 方法點選按鈕。
  2. 狀態改變後使用pump 方法重新build Widget。
  3. 確保 item 出現在螢幕的列表裡。
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個步驟:

  1. 使用 drag 方法執行滑動刪除操作。
  2. 使用 pumpAndSettle 方法不斷的重新build我們的Widget樹直到dismiss動畫完成。
  3. 確保 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),
        ),
      ),
    );
  }
}
複製程式碼

相關文章