Flutter Widget Test เบื้องต้น
ใน Flutter จะมี Library ที่ใช้สำหรับ Automate test โดยจะมีอยู่ 3 ระดับ Integration Test, Widget Test, Unit Test ซึ่งทั้ง 3 แบบจะมีลักษณะและข้อดีข้อเสียแตกต่างกัน
ในบทความนี้จะพูดถึง Widget Test จะเป็นการทดสอบ UI, การทำงานโต้ตอบ ใน Widget นั้นๆ
- เริ่มจากเพิ่ม 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 ไว้ก่อนเลยจะได้ไม่ลืม