ํญ๋ชฉ ๋ด์ฉ
Invoke /test:widget
Aliases /widget:test, /test:ui
Tools Read, Edit, Write, Glob, Grep
Model sonnet
Skills test
Specialized agent for Widget rendering testing
Role #
Tests Widget rendering and interactions.
Uses WidgetTester
pump, pumpAndSettle Pattern
find.byType, find.text, find.byKey matchers
Golden Test (Optional)
Activation Conditions #
/test:widget Activated when command is invoked
Invoked when writing Widget and Page UI tests
Parameters #
Parameter Required Description
target_widgetโ
Test target Widget Class name
feature_nameโ Feature module name
include_goldenโ Golden Test Includes Whether (default: false)
Test File Structure #
feature/ { module_type} / { feature_name} / test/
โโโ src/
โ โโโ widget/
โ โ โโโ page/
โ โ โ โโโ { feature} _page_test . dart
โ โ โโโ component/
โ โ โโโ { feature} _card_test . dart
โ โ โโโ { feature} _list_item_test . dart
โ โโโ golden/
โ โ โโโ { feature} _golden_test . dart
โ โโโ fixture/
โ โโโ { feature} _fixture . dart
โ โโโ test_app_wrapper. dart
โโโ { feature} _test . dart # Test entry point
Import Order (Required) #
// 1. Flutter test
import 'package:flutter/material.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
// 2. BLoC test (when needed)
import 'package:bloc_test/bloc_test.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
// 3. Mock package
import 'package:mockito/annotations.dart' ;
import 'package:mockito/mockito.dart' ;
// 4. Test target
import 'package:{feature}/src/presentation/page/{feature}_page.dart' ;
import 'package:{feature}/src/presentation/bloc/{feature}_bloc.dart' ;
// 5. Generated files
import '{feature}_page_test.mocks.dart' ;
Core Patterns #
import 'package:flutter/material.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
import 'package:feature_home/src/presentation/widget/user_card.dart' ;
void main ( ) {
group ( 'UserCard' , ( ) {
testWidgets ( 'renders user name and email' , ( tester) async {
// Arrange
const user = User ( id: 1 , name: 'ํ๊ธธ๋' , email: 'hong@example.com' ) ;
// Act
await tester. pumpWidget (
const MaterialApp (
home: Scaffold (
body: UserCard ( user: user) ,
) ,
) ,
) ;
// Assert
expect ( find. text ( 'ํ๊ธธ๋' ) , findsOneWidget) ;
expect ( find. text ( 'hong@example.com' ) , findsOneWidget) ;
} ) ;
testWidgets ( 'calls onTap when tapped' , ( tester) async {
// Arrange
var tapped = false ;
const user = User ( id: 1 , name: 'ํ๊ธธ๋' , email: 'hong@example.com' ) ;
// Act
await tester. pumpWidget (
MaterialApp (
home: Scaffold (
body: UserCard (
user: user,
onTap: ( ) => tapped = true ,
) ,
) ,
) ,
) ;
await tester. tap ( find. byType ( UserCard ) ) ;
await tester. pump ( ) ;
// Assert
expect ( tapped, isTrue) ;
} ) ;
} ) ;
}
import 'package:bloc_test/bloc_test.dart' ;
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
import 'package:mockito/annotations.dart' ;
import 'package:mockito/mockito.dart' ;
import 'package:feature_home/src/presentation/bloc/home_bloc.dart' ;
import 'package:feature_home/src/presentation/page/home_page.dart' ;
import 'home_page_test.mocks.dart' ;
@GenerateNiceMocks ( [ MockSpec < HomeBloC > ( ) ] )
void main ( ) {
late MockHomeBloC mockBloC;
setUp ( ( ) {
mockBloC = MockHomeBloC ( ) ;
} ) ;
Widget buildTestWidget ( ) {
return MaterialApp (
home: BlocProvider < HomeBloC > . value (
value: mockBloC,
child: const HomePage ( ) ,
) ,
) ;
}
group ( 'HomePage' , ( ) {
testWidgets ( 'shows loading indicator when state is HomeLoading' ,
( tester) async {
// Arrange
when ( mockBloC. state) . thenReturn ( const HomeLoading ( ) ) ;
when ( mockBloC. stream) . thenAnswer ( ( _) => const Stream . empty ( ) ) ;
// Act
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Assert
expect ( find. byType ( CircularProgressIndicator ) , findsOneWidget) ;
} ) ;
testWidgets ( 'shows user data when state is HomeLoaded' , ( tester) async {
// Arrange
const user = User ( id: 1 , name: 'ํ๊ธธ๋' , email: 'hong@example.com' ) ;
when ( mockBloC. state) . thenReturn ( const HomeLoaded ( user: user) ) ;
when ( mockBloC. stream) . thenAnswer ( ( _) => const Stream . empty ( ) ) ;
// Act
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Assert
expect ( find. text ( 'ํ๊ธธ๋' ) , findsOneWidget) ;
expect ( find. text ( 'hong@example.com' ) , findsOneWidget) ;
} ) ;
testWidgets ( 'shows error message when state is HomeError' , ( tester) async {
// Arrange
when ( mockBloC. state) . thenReturn (
const HomeError ( failure: ServerFailure ( message: '์๋ฒ ์ค๋ฅ' ) ) ,
) ;
when ( mockBloC. stream) . thenAnswer ( ( _) => const Stream . empty ( ) ) ;
// Act
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Assert
expect ( find. text ( '์๋ฒ ์ค๋ฅ' ) , findsOneWidget) ;
} ) ;
testWidgets ( 'adds LoadUser event on init' , ( tester) async {
// Arrange
when ( mockBloC. state) . thenReturn ( const HomeInitial ( ) ) ;
when ( mockBloC. stream) . thenAnswer ( ( _) => const Stream . empty ( ) ) ;
// Act
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Assert
verify ( mockBloC. add ( const HomeEvent . loadUser ( id: 1 ) ) ) . called ( 1 ) ;
} ) ;
} ) ;
}
testWidgets ( 'validates email input' , ( tester) async {
// Arrange
await tester. pumpWidget (
const MaterialApp (
home: Scaffold (
body: LoginForm ( ) ,
) ,
) ,
) ;
// Act - ์ ํจํ์ง ์์ ์ด๋ฉ์ผ ์
๋ ฅ
await tester. enterText (
find. byKey ( const Key ( 'email_field' ) ) ,
'invalid-email' ,
) ;
await tester. tap ( find. byKey ( const Key ( 'submit_button' ) ) ) ;
await tester. pumpAndSettle ( ) ;
// Assert
expect ( find. text ( '์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค' ) , findsOneWidget) ;
} ) ;
testWidgets ( 'submits form with valid data' , ( tester) async {
// Arrange
var submitted = false ;
String ? submittedEmail;
String ? submittedPassword;
await tester. pumpWidget (
MaterialApp (
home: Scaffold (
body: LoginForm (
onSubmit: ( email , password) {
submitted = true ;
submittedEmail = email;
submittedPassword = password;
} ,
) ,
) ,
) ,
) ;
// Act
await tester. enterText (
find. byKey ( const Key ( 'email_field' ) ) ,
'test@example.com' ,
) ;
await tester. enterText (
find. byKey ( const Key ( 'password_field' ) ) ,
'password123' ,
) ;
await tester. tap ( find. byKey ( const Key ( 'submit_button' ) ) ) ;
await tester. pumpAndSettle ( ) ;
// Assert
expect ( submitted, isTrue) ;
expect ( submittedEmail, 'test@example.com' ) ;
expect ( submittedPassword, 'password123' ) ;
} ) ;
testWidgets ( 'loads more items when scrolled to bottom' , ( tester) async {
// Arrange
when ( mockBloC. state) . thenReturn (
HomeLoaded ( users: List . generate ( 20 , ( i) => User ( id: i, name: 'User $ i ' ) ) ) ,
) ;
when ( mockBloC. stream) . thenAnswer ( ( _) => const Stream . empty ( ) ) ;
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Act - ์คํฌ๋กค์ ๋งจ ์๋๋ก
await tester. drag (
find. byType ( ListView ) ,
const Offset ( 0 , - 500 ) ,
) ;
await tester. pumpAndSettle ( ) ;
// Assert
verify ( mockBloC. add ( const HomeEvent . loadMore ( ) ) ) . called ( 1 ) ;
} ) ;
testWidgets ( 'shows all list items' , ( tester) async {
// Arrange
final users = [
const User ( id: 1 , name: 'ํ๊ธธ๋' ) ,
const User ( id: 2 , name: '๊น์ฒ ์' ) ,
const User ( id: 3 , name: '์ด์ํฌ' ) ,
] ;
when ( mockBloC. state) . thenReturn ( HomeLoaded ( users: users) ) ;
when ( mockBloC. stream) . thenAnswer ( ( _) => const Stream . empty ( ) ) ;
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Assert
expect ( find. byType ( UserListItem ) , findsNWidgets ( 3 ) ) ;
expect ( find. text ( 'ํ๊ธธ๋' ) , findsOneWidget) ;
expect ( find. text ( '๊น์ฒ ์' ) , findsOneWidget) ;
expect ( find. text ( '์ด์ํฌ' ) , findsOneWidget) ;
} ) ;
5. Dialog/Snackbar Test #
testWidgets ( 'shows confirmation dialog on delete' , ( tester) async {
// Arrange
await tester. pumpWidget ( buildTestWidget ( ) ) ;
// Act
await tester. tap ( find. byKey ( const Key ( 'delete_button' ) ) ) ;
await tester. pumpAndSettle ( ) ;
// Assert
expect ( find. byType ( AlertDialog ) , findsOneWidget) ;
expect ( find. text ( '์ญ์ ํ์๊ฒ ์ต๋๊น?' ) , findsOneWidget) ;
expect ( find. text ( 'ํ์ธ' ) , findsOneWidget) ;
expect ( find. text ( '์ทจ์' ) , findsOneWidget) ;
} ) ;
testWidgets ( 'shows snackbar after successful action' , ( tester) async {
// Arrange
when ( mockBloC. state) . thenReturn ( const HomeLoaded ( user: tUser) ) ;
when ( mockBloC. stream) . thenAnswer (
( _) => Stream . value ( const HomeActionSuccess ( message: '์ ์ฅ๋์์ต๋๋ค' ) ) ,
) ;
await tester. pumpWidget ( buildTestWidget ( ) ) ;
await tester. pumpAndSettle ( ) ;
// Assert
expect ( find. byType ( SnackBar ) , findsOneWidget) ;
expect ( find. text ( '์ ์ฅ๋์์ต๋๋ค' ) , findsOneWidget) ;
} ) ;
6. Navigation Test #
testWidgets ( 'navigates to detail page on item tap' , ( tester) async {
// Arrange
await tester. pumpWidget (
MaterialApp (
routes: {
'/' : ( _) => const HomePage ( ) ,
'/detail' : ( _) => const DetailPage ( ) ,
} ,
) ,
) ;
// Act
await tester. tap ( find. byType ( UserCard ) . first) ;
await tester. pumpAndSettle ( ) ;
// Assert
expect ( find. byType ( DetailPage ) , findsOneWidget) ;
expect ( find. byType ( HomePage ) , findsNothing) ;
} ) ;
testWidgets ( 'pops with result when confirmed' , ( tester) async {
// Arrange
Object ? result;
await tester. pumpWidget (
MaterialApp (
home: Builder (
builder: ( context) => ElevatedButton (
onPressed: ( ) async {
result = await Navigator . push (
context,
MaterialPageRoute ( builder: ( _) => const ConfirmDialog ( ) ) ,
) ;
} ,
child: const Text ( 'Open' ) ,
) ,
) ,
) ,
) ;
// Act
await tester. tap ( find. text ( 'Open' ) ) ;
await tester. pumpAndSettle ( ) ;
await tester. tap ( find. text ( 'ํ์ธ' ) ) ;
await tester. pumpAndSettle ( ) ;
// Assert
expect ( result, isTrue) ;
} ) ;
7. TestApp Wrapper Pattern #
/// ํ
์คํธ์ฉ ์ฑ ๋ํผ
///
/// ๊ณตํต ์ค์ (ํ
๋ง, ๋ก์ผ์ผ, ์์กด์ฑ)์ ํฌํจํฉ๋๋ค.
class TestAppWrapper extends StatelessWidget {
const TestAppWrapper ( {
required this . child,
this . locale = const Locale ( 'ko' ) ,
this . themeMode = ThemeMode . light,
super . key,
} ) ;
final Widget child;
final Locale locale;
final ThemeMode themeMode;
@override
Widget build ( BuildContext context) {
return MaterialApp (
locale: locale,
themeMode: themeMode,
theme: AppTheme . light,
darkTheme: AppTheme . dark,
localizationsDelegates: const [
GlobalMaterialLocalizations . delegate,
GlobalWidgetsLocalizations . delegate,
GlobalCupertinoLocalizations . delegate,
] ,
supportedLocales: const [ Locale ( 'ko' ) , Locale ( 'en' ) ] ,
home: Scaffold ( body: child) ,
) ;
}
}
// Usage example
testWidgets ( 'renders correctly in dark mode' , ( tester) async {
await tester. pumpWidget (
TestAppWrapper (
themeMode: ThemeMode . dark,
child: const UserCard ( user: tUser) ,
) ,
) ;
// ํ
์คํธ ๋ก์ง...
} ) ;
8. Golden Test #
import 'package:flutter/material.dart' ;
import 'package:flutter_test/flutter_test.dart' ;
import 'package:golden_toolkit/golden_toolkit.dart' ;
import '../fixture/test_app_wrapper.dart' ;
void main ( ) {
group ( 'UserCard Golden Tests' , ( ) {
testGoldens ( 'UserCard matches golden file' , ( tester) async {
// Arrange
const user = User ( id: 1 , name: 'ํ๊ธธ๋' , email: 'hong@example.com' ) ;
final builder = GoldenBuilder . column ( )
.. addScenario (
'Default' ,
const UserCard ( user: user) ,
)
.. addScenario (
'With avatar' ,
const UserCard ( user: user, showAvatar: true ) ,
)
.. addScenario (
'Compact' ,
const UserCard ( user: user, compact: true ) ,
) ;
// Act & Assert
await tester. pumpWidgetBuilder (
builder. build ( ) ,
wrapper: materialAppWrapper ( theme: AppTheme . light) ,
) ;
await screenMatchesGolden ( tester, 'user_card' ) ;
} ) ;
testGoldens ( 'UserCard responsive variants' , ( tester) async {
const user = User ( id: 1 , name: 'ํ๊ธธ๋' , email: 'hong@example.com' ) ;
await tester. pumpWidgetBuilder (
const UserCard ( user: user) ,
wrapper: materialAppWrapper ( theme: AppTheme . light) ,
) ;
await multiScreenGolden (
tester,
'user_card_responsive' ,
devices: [
Device . phone,
Device . iphone11,
Device . tabletLandscape,
] ,
) ;
} ) ;
} ) ;
}
find Matcher Summary #
Matcher Purpose Example
find.text()Text search find.text('ํ๊ธธ๋')
find.byType()
Search by type
find.byType(UserCard)
find.byKey()
Search by Key
find.byKey(Key('email'))
find.byIcon()
Icon search
find.byIcon(Icons.delete)
find.byWidget()
Widget instance search
find.byWidget(myWidget)
find.descendant()
Descendant search
find.descendant(of: ..., matching: ...)
find.ancestor()
Ancestor search
find.ancestor(of: ..., matching: ...)
find.byWidgetPredicate()
Search by predicate
find.byWidgetPredicate((w) => ...)
expect Matcher Summary #
Matcher Purpose Example
findsOneWidget
Exactly 1
expect(find.text('ํ๊ธธ๋'), findsOneWidget)
findsNothing
0 items
expect(find.text('์์'), findsNothing)
findsWidgets
1 or more
expect(find.byType(Card), findsWidgets)
findsNWidgets(n)
Exactly n
expect(find.byType(Item), findsNWidgets(3))
findsAtLeast(n)
At least n
expect(find.byType(Item), findsAtLeast(2))
pump Method Summary #
Method Purpose Example
pump()Single frame render await tester.pump()
pump(duration)
Advance by specified duration
await tester.pump(Duration(seconds: 1))
pumpAndSettle()
Wait until animations complete
await tester.pumpAndSettle()
pumpWidget()
Render widget
await tester.pumpWidget(widget)
Build Commands #
# Widget ํ
์คํธ๋ง ์คํ
flutter test test/ src/ widget/
# Golden ํ
์คํธ ์
๋ฐ์ดํธ
flutter test -- update- goldens test/ src/ golden/
# ํน์ ์์ ฏ ํ
์คํธ ์คํ
flutter test test/ src/ widget/ page/ home_page_test. dart
# Test with coverage
melos run test: with - html- coverage
Reference Files #
feature/ application/ home/ test/ src/ widget/ page/ home_page_test. dart
feature/ application/ home/ test/ src/ widget/ component/ user_card_test. dart
feature/ common/ auth/ test/ src/ widget/ login_page_test. dart
Checklist #