Flutter Widget Test เบื้องต้น

Darthnot
Mintelligence
Published in
4 min readMay 30, 2024

ใน Flutter จะมี Library ที่ใช้สำหรับ Automate test โดยจะมีอยู่ 3 ระดับ Integration Test, Widget Test, Unit Test ซึ่งทั้ง 3 แบบจะมีลักษณะและข้อดีข้อเสียแตกต่างกัน

ในบทความนี้จะพูดถึง Widget Test จะเป็นการทดสอบ UI, การทำงานโต้ตอบ ใน Widget นั้นๆ

  1. เริ่มจากเพิ่ม flutter_test ใน dev_dependencies (อาจจะเพิ่มการเพิ่มอัตโนมัติตอนสร้างโปรเจคแล้ว)
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0

2. ทำการสร้าง Widget ในโปรเจค

ณ ตัวอย่างนี้จะมี 2 Widget คือ counter, header

import 'package:flutter/material.dart';
import 'package:flutter_test_decendency/src/widgets/header.dart';

class Counter extends StatefulWidget {
const Counter({super.key, required this.title});

final String title;

@override
State<Counter> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<Counter> {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Header(text: widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
import 'package:flutter/material.dart';

class Header extends StatelessWidget {
String text;
Header({super.key, required this.text});

@override
Widget build(BuildContext context) {
return Container(
child: Text(
text,
style: const TextStyle(color: Colors.red),
),
);
}
}

โดยแอพจะมีหน้าประมานนี้

3. เพิ่ม WidgetTester ที่ test/test_widget.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_decendency/src/widgets/counter.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Counter(
title: 'Title1',
),
),
));
});
}

4. ค้นหา Element ต่างๆใน Widget โดยใช้ Finder

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_decendency/src/widgets/counter.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Counter(
title: 'Title1',
),
),
));

Finder zeroText = find.text('0');
Finder oneText = find.text('1');
});
}

5. ตรวจสอบ Element ที่ค้นหาจากข้อ 4 โดยใช้ Matcher

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_decendency/src/widgets/counter.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Counter(
title: 'Title1',
),
),
));

Finder zeroText = find.text('0');
Finder oneText = find.text('1');

// Verify that our counter starts at 0.
expect(zeroText, findsOneWidget);
expect(oneText, findsNothing);
});
}

กดรันจากไอคอนรันเทสหรือใช้ flutter test

จากนั้นจะเพิ่มการกดปุ่มแล้ว Match อีกที

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_decendency/src/widgets/counter.dart';

void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Counter(
title: 'Title1',
),
),
));

Finder zeroText = find.text('0');
Finder oneText = find.text('1');

// Verify that our counter starts at 0.
expect(zeroText, findsOneWidget);
expect(oneText, findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(zeroText, findsOneWidget);
});
}

จากโค้ดด้านบนจะเทสไม่ผ่าน

เพราะหลังจากสั่งกดปุ่มเลขที่แสดงบนแอพจะแสดงเป็น 1 จึง Match ไม่เจอ ต้องเลี่ยนเป็นแบบภาพด้านล่างก็จะผ่าน

testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: Counter(
title: 'Title1',
),
),
));

Finder zeroText = find.text('0');
Finder oneText = find.text('1');

// Verify that our counter starts at 0.
expect(zeroText, findsOneWidget);
expect(oneText, findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(zeroText, findsNothing);
expect(oneText, findsOneWidget);
});

โดย Matcher จะมีหลายแบบให้ใช้

  • หลังจากนี้จะเป็นการเพิ่ม Test Case หลายๆ Case หรือเพิ่ม Matcher หลายๆอันตามต้องการโดยการใช้ group
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_decendency/src/widgets/counter.dart';
import 'package:flutter_test_decendency/src/widgets/header.dart';

void main() {
group('Counter Widget', () {
late Widget testWidget;

setUp(() {
testWidget = const MaterialApp(
home: Scaffold(
body: Counter(
title: 'Title1',
),
),
);
});

testWidgets('Check initial wording', (WidgetTester tester) async {
await tester.pumpWidget(testWidget);

Finder text = find.text('You have pushed the button this many times:');

expect(text, findsOneWidget);
});

testWidgets('Check increase button', (WidgetTester tester) async {
await tester.pumpWidget(testWidget);

final iconFinder = find.byType(Icon);
expect(iconFinder, findsOneWidget);

final iconSize = tester.getSize(iconFinder);
expect(iconSize.width, 20.0);
expect(iconSize.height, 20.0);
});

testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(testWidget);

Finder zeroText = find.text('0');
Finder oneText = find.text('1');

// Verify that our counter starts at 0.
expect(zeroText, findsOneWidget);
expect(oneText, findsNothing);

// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();

// Verify that our counter has incremented.
expect(zeroText, findsNothing);
expect(oneText, findsOneWidget);
});
});

group('Header Widget', () {
late Widget testWidget;

setUp(() {
testWidget = MaterialApp(
home: Scaffold(
body: Header(
text: 'title1',
)),
);
});

testWidgets('Check text display', (WidgetTester tester) async {
await tester.pumpWidget(testWidget);
Finder text = find.text('title1');
expect(text, findsOneWidget);
});

testWidgets('Check text color', (WidgetTester tester) async {
await tester.pumpWidget(testWidget);
Finder text = find.text('title1');
expect(text, findsOneWidget);
final textWidget = tester.widget<Text>(text);
expect(textWidget.style?.color, Colors.red);
});
});
}
  • แนะนำในการสร้าง Widget แต่ละอันให้เริ่มจากการสร้าง Widget สำหรับ Test ไว้ก่อนเลยจะได้ไม่ลืม

--

--