วิธีทดสอบแอป Flutter

1. บทนำ

Flutter คือชุดเครื่องมือ UI ของ Google สำหรับการสร้างแอปพลิเคชันที่สวยงามซึ่งรวบรวมไว้ภายในอุปกรณ์เคลื่อนที่ เว็บ และเดสก์ท็อปจากฐานของโค้ดเดียว

ใน Codelab นี้ คุณจะได้สร้างและทดสอบแอป Flutter แบบง่ายๆ แอปจะใช้แพ็กเกจผู้ให้บริการสำหรับการจัดการสถานะ

สิ่งที่คุณจะได้เรียนรู้

  • วิธีสร้างการทดสอบวิดเจ็ตโดยใช้เฟรมเวิร์กการทดสอบวิดเจ็ต
  • วิธีสร้างการทดสอบการผสานรวมเพื่อทดสอบ UI และประสิทธิภาพของแอปโดยใช้ไลบรารี integration_test
  • วิธีทดสอบคลาสข้อมูล (ผู้ให้บริการ) โดยใช้การทดสอบ 1 หน่วย

สิ่งที่คุณจะสร้าง

ใน Codelab นี้ คุณจะเริ่มต้นด้วยการสร้างแอปพลิเคชันง่ายๆ ที่มีรายการรายการต่างๆ เราให้ซอร์สโค้ดแก่คุณเพื่อให้คุณเข้าสู่การทดสอบได้ทันที แอปสนับสนุนการทำงานต่อไปนี้

  • การเพิ่มรายการลงในรายการโปรด
  • การดูรายการโปรด
  • การนำรายการออกจากรายการโปรด

เมื่อแอปเสร็จสมบูรณ์แล้ว คุณจะเขียนการทดสอบต่อไปนี้

  • การทดสอบหน่วยเพื่อตรวจสอบการดำเนินการเพิ่มและนำออก
  • การทดสอบวิดเจ็ตสำหรับหน้าแรกและหน้าโปรด
  • การทดสอบ UI และประสิทธิภาพสำหรับทั้งแอปโดยใช้การทดสอบการผสานรวม

GIF ของแอปที่ทำงานบน Android

คุณต้องการเรียนรู้อะไรจาก Codelab นี้

ฉันเพิ่งเริ่มหัดทำและอยากได้ภาพรวมที่มีประโยชน์ ฉันรู้เรื่องเกี่ยวกับหัวข้อนี้ แต่ต้องการทบทวนความรู้ ฉันต้องการตัวอย่างโค้ดที่จะใช้ในโปรเจ็กต์ ฉันต้องการคำอธิบายบางอย่างที่เจาะจง

2. ตั้งค่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ Flutter

ห้องทดลองนี้ต้องมีซอฟต์แวร์ 2 ประเภท ได้แก่ Flutter SDK และเครื่องมือแก้ไข

คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้

  • อุปกรณ์ Android หรือ iOS ที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาซอฟต์แวร์
  • เครื่องมือจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
  • โปรแกรมจำลอง Android (ต้องตั้งค่าใน Android Studio)
  • เบราว์เซอร์ (การแก้ไขข้อบกพร่องต้องใช้ Chrome)
  • เป็นแอปพลิเคชัน Windows, Linux หรือ macOS บนเดสก์ท็อป คุณต้องพัฒนาบนแพลตฟอร์มที่คุณวางแผนจะทำให้ใช้งานได้ ดังนั้นหากต้องการพัฒนาแอป Windows บนเดสก์ท็อป คุณต้องพัฒนาบน Windows เพื่อเข้าถึงเชนบิลด์ที่เหมาะสม มีข้อกำหนดเฉพาะระบบปฏิบัติการที่ครอบคลุมรายละเอียดใน docs.flutter.dev/desktop

3. เริ่มต้นใช้งาน

สร้างแอป Flutter ใหม่และ อัปเดตการอ้างอิง

Codelab นี้มุ่งเน้นไปที่การทดสอบแอปบนอุปกรณ์เคลื่อนที่ Flutter คุณจะสร้างแอปที่จะทดสอบได้อย่างรวดเร็วโดยใช้ไฟล์ต้นฉบับที่คุณคัดลอกและวาง จากนั้น Codelab ที่เหลือจะมุ่งเน้นที่การเรียนรู้การทดสอบประเภทต่างๆ

a3c16fc17be25f6c.pngสร้างแอป Flutter ที่ใช้เทมเพลตง่ายๆ ตามวิธีการในการเริ่มต้นใช้งานแอป Flutter แรกของคุณ หรือบรรทัดคำสั่งต่อไปนี้

$ flutter create --empty testing_app
Creating project testing_app...
Resolving dependencies in `testing_app`... 
Downloading packages... 
Got dependencies in `testing_app`.
Wrote 128 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

In order to run your empty application, type:

  $ cd testing_app
  $ flutter run

Your empty application code is in testing_app/lib/main.dart.

a3c16fc17be25f6c.pngเพิ่มทรัพยากร Dependency ของ pub ในบรรทัดคำสั่ง

  • provider เพื่อการจัดการสถานะที่ง่ายดาย
  • integration_test สำหรับการทดสอบโค้ด Flutter ด้วยตนเองในอุปกรณ์และโปรแกรมจำลอง
  • flutter_driver สำหรับ API ขั้นสูงเพื่อทดสอบแอปพลิเคชัน Flutter ที่ทำงานในอุปกรณ์และโปรแกรมจำลองจริง
  • test สำหรับเครื่องมือทดสอบทั่วไป
  • go_router สำหรับการจัดการการไปยังส่วนต่างๆ ของแอป
$ cd testing_app
$ flutter pub add provider go_router dev:test 'dev:flutter_driver:{"sdk":"flutter"}' 'dev:integration_test:{"sdk":"flutter"}'
Resolving dependencies... 
Downloading packages... 
+ _fe_analyzer_shared 67.0.0 (68.0.0 available)
+ analyzer 6.4.1 (6.5.0 available)
+ args 2.5.0
+ convert 3.1.1
+ coverage 1.7.2
+ crypto 3.0.3
+ file 7.0.0
+ flutter_driver 0.0.0 from sdk flutter
+ flutter_web_plugins 0.0.0 from sdk flutter
+ frontend_server_client 4.0.0
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ glob 2.1.2
+ go_router 14.0.2
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ integration_test 0.0.0 from sdk flutter
+ io 1.0.4
+ js 0.7.1
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
+ logging 1.2.0
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ mime 1.0.5
+ nested 1.0.0
+ node_preamble 2.0.2
+ package_config 2.1.0
+ platform 3.1.4
+ pool 1.5.1
+ process 5.0.2
+ provider 6.1.2
+ pub_semver 2.1.4
+ shelf 1.4.1
+ shelf_packages_handler 3.0.2
+ shelf_static 1.1.2
+ shelf_web_socket 1.0.4
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.12
+ sync_http 0.3.1
+ test 1.25.2 (1.25.4 available)
  test_api 0.7.0 (0.7.1 available)
+ test_core 0.6.0 (0.6.2 available)
+ typed_data 1.3.2
+ watcher 1.1.0
+ web 0.5.1
+ web_socket_channel 2.4.5
+ webdriver 3.0.3
+ webkit_inspection_protocol 1.2.1
+ yaml 3.1.2
Changed 44 dependencies!
9 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

ควรเพิ่มทรัพยากร Dependency ต่อไปนี้ใน pubspec.yaml แล้ว

pubspec.yaml

name: testing_app
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.4.0-0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.2
  provider: ^6.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  test: ^1.25.2
  flutter_driver:
    sdk: flutter
  integration_test:
    sdk: flutter

flutter:
  uses-material-design: true

a3c16fc17be25f6c.pngเปิดโปรเจ็กต์ในตัวแก้ไขโค้ดที่ต้องการ แล้วเรียกใช้แอป หรือเรียกใช้ในบรรทัดคำสั่งดังต่อไปนี้

$ flutter run

4. สร้างแอป

ถัดไป คุณจะต้องสร้างแอปเพื่อทำการทดสอบ แอปประกอบด้วยไฟล์ต่อไปนี้

  • lib/models/favorites.dart - สร้างคลาสโมเดลสำหรับรายการโปรด
  • lib/screens/favorites.dart - สร้างเลย์เอาต์สำหรับรายการโปรด
  • lib/screens/home.dart - สร้างรายการข้อมูล
  • lib/main.dart - ไฟล์หลักที่แอปเริ่มทำงาน

ขั้นแรก ให้สร้างโมเดล Favorites ใน lib/models/favorites.dart

a3c16fc17be25f6c.pngสร้างไดเรกทอรีใหม่ชื่อ models ในไดเรกทอรี lib แล้วสร้างไฟล์ใหม่ชื่อ favorites.dart ในไฟล์ดังกล่าว ให้เพิ่มโค้ดต่อไปนี้

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

เพิ่มหน้ารายการโปรดใน lib/screens/favorites.dart

a3c16fc17be25f6c.pngสร้างไดเรกทอรีใหม่ชื่อ screens ในไดเรกทอรี lib และสร้างไฟล์ใหม่ชื่อ favorites.dart ในไดเรกทอรีดังกล่าว ในไฟล์ดังกล่าว ให้เพิ่มโค้ดต่อไปนี้

lib/screens/favorites.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/favorites.dart';

class FavoritesPage extends StatelessWidget {
  const FavoritesPage({super.key});

  static String routeName = 'favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  const FavoriteItemTile(this.itemNo, {super.key});

  final int itemNo;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: const Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

เพิ่มหน้าแรกใน lib/screens/home.dart

a3c16fc17be25f6c.pngสร้างไฟล์ใหม่ชื่อ home.dart ในไดเรกทอรี lib/screens ใน lib/screens/home.dart ให้เพิ่มโค้ดต่อไปนี้

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/favorites.dart';
import 'favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () {
              context.go('/${FavoritesPage.routeName}');
            },
            icon: const Icon(Icons.favorite_border),
            label: const Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(this.itemNo, {super.key});

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? const Icon(Icons.favorite)
              : const Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: const Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

แทนที่เนื้อหาของ lib/main.dart

a3c16fc17be25f6c.pngแทนที่เนื้อหาของ lib/main.dart ด้วยรหัสต่อไปนี้

lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'models/favorites.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';

void main() {
  runApp(const TestingApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: HomePage.routeName,
      builder: (context, state) {
        return const HomePage();
      },
      routes: [
        GoRoute(
          path: FavoritesPage.routeName,
          builder: (context, state) {
            return const FavoritesPage();
          },
        ),
      ],
    ),
  ],
);

class TestingApp extends StatelessWidget {
  const TestingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp.router(
        title: 'Testing Sample',
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: Colors.deepPurple,
          ),
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

แอปเสร็จสมบูรณ์แล้วแต่ยังไม่ผ่านการทดสอบ

a3c16fc17be25f6c.pngเรียกใช้แอป ซึ่งควรมีหน้าตาเหมือนกับภาพหน้าจอต่อไปนี้

b74f843e42a28b0f.png

แอปจะแสดงรายการต่างๆ แตะไอคอนรูปหัวใจในแถวใดก็ได้เพื่อใส่หัวใจและเพิ่มไอเทมลงในรายการโปรด ปุ่มรายการโปรดใน AppBar จะนำคุณไปยังหน้าจอที่ 2 ที่มีรายการโปรด

ตอนนี้แอปพร้อมสำหรับการทดสอบแล้ว คุณจะเริ่มทดสอบแอปในขั้นตอนถัดไป

5. การทดสอบหน่วยวัดของผู้ให้บริการ

คุณจะเริ่มต้นโดยการทดสอบ 1 หน่วยของโมเดล favorites การทดสอบ 1 หน่วยคืออะไร การทดสอบ 1 หน่วยจะตรวจสอบว่าซอฟต์แวร์แต่ละหน่วยนั้นเป็นฟังก์ชัน ออบเจ็กต์ หรือวิดเจ็ต ทำงานตามที่ต้องการได้อย่างถูกต้อง

ไฟล์ทดสอบทั้งหมดในแอป Flutter จะอยู่ในไดเรกทอรี test ยกเว้นการทดสอบการผสานรวม

นำ test/widget_test.dart ออก

a3c16fc17be25f6c.pngก่อนที่จะเริ่มทดสอบ ให้ลบไฟล์ widget_test.dart คุณจะเพิ่มไฟล์ทดสอบของคุณเอง

สร้างไฟล์ทดสอบใหม่

ก่อนอื่น คุณจะต้องทดสอบเมธอด add() ในโมเดล Favorites เพื่อยืนยันว่ามีการเพิ่มรายการใหม่ลงในรายการ และรายการแสดงถึงการเปลี่ยนแปลง ตามปกติแล้ว โครงสร้างไดเรกทอรีในไดเรกทอรี test จะเลียนแบบว่าในไดเรกทอรี lib และไฟล์ Dart มีชื่อเดียวกันโดยมี _test ต่อท้าย

a3c16fc17be25f6c.pngสร้างไดเรกทอรี models ในไดเรกทอรี test ในไดเรกทอรีใหม่นี้ ให้สร้างไฟล์ favorites_test.dart ที่มีเนื้อหาต่อไปนี้

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });    
  });
}

เฟรมเวิร์กการทดสอบ Flutter ช่วยให้คุณเชื่อมโยงการทดสอบที่คล้ายกันซึ่งเกี่ยวข้องกันในกลุ่มได้ ไฟล์ทดสอบไฟล์เดียวอาจมีได้หลายกลุ่มเพื่อทดสอบส่วนต่างๆ ของไฟล์ที่เกี่ยวข้องในไดเรกทอรี lib

เมธอด test() ใช้พารามิเตอร์ตำแหน่ง 2 ตัว ได้แก่ description ของการทดสอบและ callback ที่คุณใช้เขียนการทดสอบจริงๆ

a3c16fc17be25f6c.pngทดสอบการนำรายการออกจากรายการ แทรกการทดสอบต่อไปนี้ในกลุ่ม Testing App Provider เดียวกัน:

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

ทำการทดสอบ

a3c16fc17be25f6c.pngไปที่ไดเรกทอรีรากของโปรเจ็กต์ในบรรทัดคำสั่ง แล้วป้อนคำสั่งต่อไปนี้

$ flutter test test/models/favorites_test.dart 

หากทุกอย่างทำงานเป็นปกติ คุณควรเห็นข้อความที่คล้ายกับข้อความต่อไปนี้

00:06 +2: All tests passed!                                                    

ไฟล์ทดสอบที่สมบูรณ์: test/models/favorites_test.dart

ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบ 1 หน่วยได้ที่ข้อมูลเบื้องต้นเกี่ยวกับการทดสอบ 1 หน่วย

6. การทดสอบวิดเจ็ต

ในขั้นตอนนี้ คุณจะต้องเพิ่มโค้ดเพื่อทดสอบวิดเจ็ต การทดสอบวิดเจ็ตมีเอกลักษณ์เฉพาะสำหรับ Flutter ซึ่งคุณจะทดสอบวิดเจ็ตแต่ละรายการได้แบบแยกส่วน ขั้นตอนนี้จะทดสอบ HomePage และ FavoritesPage หน้าจอแยกกัน

การทดสอบวิดเจ็ตใช้ฟังก์ชัน testWidget() แทนฟังก์ชัน test() เช่นเดียวกับฟังก์ชัน test() ฟังก์ชัน testWidget() จะใช้พารามิเตอร์ 2 รายการ ได้แก่ description, และ callback แต่ Callback จะใช้ WidgetTester เป็นอาร์กิวเมนต์

การทดสอบวิดเจ็ตใช้ TestFlutterWidgetsBinding ซึ่งเป็นคลาสที่ให้ทรัพยากรเดียวกันกับวิดเจ็ตที่จะมีในแอปที่ทำงานอยู่ เช่น ข้อมูลเกี่ยวกับขนาดหน้าจอ ความสามารถในการตั้งเวลาภาพเคลื่อนไหว โดยไม่ต้องทำงานภายในแอป ระบบจะใช้สภาพแวดล้อมเสมือนในการสร้างวิดเจ็ต แล้วทำการทดสอบผลลัพธ์แทน ในส่วนนี้ pumpWidget ได้เริ่มต้นกระบวนการด้วยการบอกให้เฟรมเวิร์กต่อเชื่อมและวัดวิดเจ็ตหนึ่งๆ เช่นเดียวกับที่ทำในแอปพลิเคชัน

เฟรมเวิร์กการทดสอบวิดเจ็ตช่วยให้เครื่องมือค้นหาค้นหาวิดเจ็ตได้ เช่น text(), byType() และ byIcon(). นอกจากนี้ เฟรมเวิร์กยังมีเครื่องมือจับคู่เพื่อตรวจสอบผลลัพธ์ด้วย

เริ่มต้นด้วยการทดสอบวิดเจ็ต HomePage

สร้างไฟล์ทดสอบใหม่

การทดสอบแรกจะตรวจสอบว่าการเลื่อน HomePage ทำงานถูกต้องหรือไม่

a3c16fc17be25f6c.pngสร้างไฟล์ใหม่ในไดเรกทอรี test และตั้งชื่อว่า home_test.dart เพิ่มโค้ดต่อไปนี้ในไฟล์ที่สร้างขึ้นใหม่

test/home_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: const MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

ใช้ฟังก์ชัน createHomeScreen() เพื่อสร้างแอปที่โหลดวิดเจ็ตที่จะทดสอบใน MaterialApp โดยรวมไว้ใน ChangeNotifierProvider วิดเจ็ตของหน้าแรกจำเป็นต้องมีทั้ง 2 วิดเจ็ตนี้ไว้ด้านบนในแผนผังวิดเจ็ต เพื่อให้วิดเจ็ตนั้นรับค่าจากวิดเจ็ตและเข้าถึงข้อมูลที่มีได้ ระบบจะส่งฟังก์ชันนี้เป็นพารามิเตอร์ไปยังฟังก์ชัน pumpWidget()

ถัดไป ให้ทดสอบว่าเฟรมเวิร์กนี้สามารถค้นหา ListView ที่แสดงผลบนหน้าจอได้หรือไม่

a3c16fc17be25f6c.pngเพิ่มข้อมูลโค้ดต่อไปนี้ใน home_test.dart:

test/home_test.dart

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {  
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });                                                
  // END OF NEW CONTENT

    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
});

ทำการทดสอบ

ขั้นแรก ให้ทำการทดสอบด้วยวิธีเดียวกับที่คุณทำการทดสอบ 1 หน่วย โดยใช้คำสั่ง:

$ flutter test test/home_test.dart 

การทดสอบควรทำงานอย่างรวดเร็ว และคุณจะเห็นข้อความต่อไปนี้:

00:02 +2: All tests passed!                                                    

นอกจากนี้ คุณยังเรียกใช้การทดสอบวิดเจ็ตโดยใช้อุปกรณ์หรือโปรแกรมจำลองได้ ซึ่งช่วยให้ดูการทดสอบที่ทำงานอยู่ได้ นอกจากนี้ยังสามารถใช้ Hot รีสตาร์ทได้ด้วย

a3c16fc17be25f6c.pngเสียบปลั๊กอุปกรณ์หรือเริ่มโปรแกรมจำลอง นอกจากนี้คุณยังทำการทดสอบเป็นแอปพลิเคชันบนเดสก์ท็อปได้อีกด้วย

a3c16fc17be25f6c.pngจากบรรทัดคำสั่ง ให้ไปที่ไดเรกทอรีรากของโปรเจ็กต์และป้อนคำสั่งต่อไปนี้

$ flutter run test/home_test.dart 

คุณอาจต้องเลือกอุปกรณ์ที่จะทำการทดสอบ ในกรณีนี้ให้ทำตามวิธีการและเลือกอุปกรณ์

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 

หากทุกอย่างทำงานได้ดี คุณควรเห็นผลลัพธ์ที่คล้ายกับข้อความต่อไปนี้

Launching test/home_test.dart on Linux in debug mode...
Building Linux application...                                           
flutter: 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Linux...                                    62ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:00 +1: Home Page Widget Tests Testing Scrolling
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:02 +2: All tests passed!

ถัดไป คุณจะต้องแก้ไขไฟล์ทดสอบ แล้วกด Shift + R เพื่อรีสตาร์ทแอปและเรียกใช้การทดสอบทั้งหมดอีกครั้ง อย่าหยุดแอปพลิเคชัน

a3c16fc17be25f6c.pngเพิ่มการทดสอบไปยังกลุ่มที่ทดสอบวิดเจ็ตในหน้าแรก คัดลอกการทดสอบต่อไปนี้ไปยังไฟล์

test/home_test.dart

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

การทดสอบนี้ยืนยันว่าการแตะIconButtonเปลี่ยนจาก Icons.favorite_border (หัวใจเปิด) เป็น Icons.favorite (หัวใจเต็มอิ่ม) จากนั้นกลับไปเป็น Icons.favorite_border เมื่อแตะอีกครั้ง

a3c16fc17be25f6c.pngป้อน Shift + R Hot นี้จะรีสตาร์ทแอปและเรียกใช้การทดสอบทั้งหมดอีกครั้ง

ไฟล์ทดสอบที่สมบูรณ์: test/home_test.dart.

a3c16fc17be25f6c.pngใช้กระบวนการเดียวกันเพื่อทดสอบ FavoritesPage ด้วยโค้ดต่อไปนี้ ทำตามขั้นตอนเดิมและเรียกใช้

test/favorites_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

การทดสอบนี้จะยืนยันว่ารายการหายไปหรือไม่เมื่อกดปุ่มปิด (นำออก)

ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบวิดเจ็ตได้ที่

7. การทดสอบ UI ของแอปด้วยการทดสอบการผสานรวม

การทดสอบการผสานรวมใช้เพื่อทดสอบว่าแอปแต่ละรายการทำงานร่วมกันอย่างไร ไลบรารี integration_test ใช้เพื่อทำการทดสอบการผสานรวมใน Flutter นี่คือเวอร์ชัน Selenium WebDriver, Protractor, Espresso หรือ Earl Gray ของ Flutter แพ็กเกจจะใช้ flutter_driver เป็นการภายในเพื่อทำการทดสอบในอุปกรณ์

การทดสอบการเขียนการผสานรวมใน Flutter จะคล้ายกับการทดสอบวิดเจ็ตการเขียน ยกเว้นว่าการทดสอบการผสานรวมจะทํางานบนอุปกรณ์เคลื่อนที่ เบราว์เซอร์ หรือแอปพลิเคชันเดสก์ท็อปที่เรียกว่าอุปกรณ์เป้าหมาย

เขียนแบบทดสอบ

a3c16fc17be25f6c.pngสร้างไดเรกทอรีชื่อ integration_test ในไดเรกทอรีรากของโครงการ แล้วสร้างไฟล์ใหม่ชื่อ app_test.dart ในไดเรกทอรีดังกล่าว

integration_test/app_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App', () {
    testWidgets('Favorites operations test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final iconKeys = [
        'icon_0',
        'icon_1',
        'icon_2',
      ];

      for (var icon in iconKeys) {
        await tester.tap(find.byKey(ValueKey(icon)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Added to favorites.'), findsOneWidget);
      }

      await tester.tap(find.text('Favorites'));
      await tester.pumpAndSettle();

      final removeIconKeys = [
        'remove_icon_0',
        'remove_icon_1',
        'remove_icon_2',
      ];

      for (final iconKey in removeIconKeys) {
        await tester.tap(find.byKey(ValueKey(iconKey)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Removed from favorites.'), findsOneWidget);
      }
    });
  });
}

ทำการทดสอบ

a3c16fc17be25f6c.pngเสียบปลั๊กอุปกรณ์หรือเริ่มโปรแกรมจำลอง นอกจากนี้คุณยังทำการทดสอบเป็นแอปพลิเคชันบนเดสก์ท็อปได้อีกด้วย

a3c16fc17be25f6c.pngไปที่ไดเรกทอรีรากของโปรเจ็กต์ในบรรทัดคำสั่ง แล้วป้อนคำสั่งต่อไปนี้

$ flutter test integration_test/app_test.dart

หากทุกอย่างทำงานได้ดี คุณควรเห็นผลลัพธ์ที่คล้ายกับข้อความต่อไปนี้

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
00:00 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                B00:08 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                
00:26 +1: All tests passed!

8. การทดสอบประสิทธิภาพของแอปด้วย Flutter Driver

เขียนการทดสอบประสิทธิภาพ

สร้างไฟล์ทดสอบใหม่ชื่อ perf_test.dart ในโฟลเดอร์ Integration_test ที่มีเนื้อหาต่อไปนี้

integration_test/perf_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App Performance', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

    testWidgets('Scrolling test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final listFinder = find.byType(ListView);

      await binding.traceAction(() async {
        await tester.fling(listFinder, const Offset(0, -500), 10000);
        await tester.pumpAndSettle();

        await tester.fling(listFinder, const Offset(0, 500), 10000);
        await tester.pumpAndSettle();
      }, reportKey: 'scrolling_summary');
    });
  });
}

ฟังก์ชัน ensureInitialized() จะตรวจสอบว่าไดรเวอร์เพื่อการทดสอบการผสานรวมเริ่มทำงานแล้วหรือยัง และเริ่มต้นการทำงานใหม่หากจำเป็น การตั้งค่า framePolicy เป็น fullyLive เหมาะสำหรับการทดสอบโค้ดภาพเคลื่อนไหว

การทดสอบนี้จะเลื่อนผ่านรายการต่างๆ อย่างรวดเร็ว แล้วเลื่อนขึ้นไปจนสุด ฟังก์ชัน traceAction() จะบันทึกการดำเนินการและสร้างสรุปไทม์ไลน์

บันทึกผลลัพธ์ด้านประสิทธิภาพ

หากต้องการเก็บผลลัพธ์ ให้สร้างโฟลเดอร์ชื่อ test_driver โดยใช้ไฟล์ชื่อ perf_driver.dart และเพิ่มโค้ดต่อไปนี้

test_driver/perf_driver.dart

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
            data['scrolling_summary'] as Map<String, dynamic>);

        final summary = driver.TimelineSummary.summarize(timeline);

        await summary.writeTimelineToFile(
          'scrolling_summary',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

ทำการทดสอบ

a3c16fc17be25f6c.pngเสียบปลั๊กอุปกรณ์หรือเริ่มโปรแกรมจำลอง

a3c16fc17be25f6c.pngไปที่ไดเรกทอรีรากของโปรเจ็กต์ในบรรทัดคำสั่ง แล้วป้อนคำสั่งต่อไปนี้

$ flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/perf_test.dart \
  --profile \
  --no-dds

หากทุกอย่างทำงานได้ดี คุณควรเห็นผลลัพธ์ที่คล้ายกับข้อความต่อไปนี้

Running "flutter pub get" in testing_app...
Resolving dependencies... 
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!
Running Gradle task 'assembleProfile'...                         1,379ms
✓  Built build/app/outputs/flutter-apk/app-profile.apk (14.9MB).
Installing build/app/outputs/flutter-apk/app-profile.apk...        222ms
I/flutter ( 6125): 00:04 +1: Testing App Performance (tearDownAll)
I/flutter ( 6125): 00:04 +2: All tests passed!
All tests passed.

หลังจากการทดสอบเสร็จสมบูรณ์แล้ว ไดเรกทอรีบิลด์ที่รูทของโปรเจ็กต์จะมีไฟล์ 2 ไฟล์ ดังนี้

  1. scrolling_summary.timeline_summary.json มีข้อมูลสรุป เปิดไฟล์ด้วยเครื่องมือแก้ไขข้อความใดก็ได้เพื่อดูข้อมูลที่มีอยู่
  2. scrolling_summary.timeline.json มีข้อมูลไทม์ไลน์ที่สมบูรณ์

ดูรายละเอียดเพิ่มเติมเกี่ยวกับการทดสอบการผสานรวมได้ที่

9. ยินดีด้วย

คุณศึกษา Codelab จนจบและเรียนรู้วิธีต่างๆ ในการทดสอบแอป Flutter แล้ว

สิ่งที่คุณได้เรียนรู้

  • วิธีทดสอบผู้ให้บริการโดยใช้การทดสอบ 1 หน่วย
  • วิธีทดสอบวิดเจ็ตโดยใช้เฟรมเวิร์กการทดสอบวิดเจ็ต
  • วิธีทดสอบ UI ของแอปโดยใช้การทดสอบการผสานรวม
  • วิธีทดสอบประสิทธิภาพของแอปโดยใช้การทดสอบการผสานรวม

ดูข้อมูลเพิ่มเติมเกี่ยวกับการทดสอบใน Flutter ได้ที่