: 각 페이지 또는 위젯을 테스트하는 방식
: 주어진 위젯을 통해 UI 렌더링
: provider의 state가 저장되어있는 위젯
ConsumerWidget와 같이 Ref가 존재하는 경우 (= provider를 사용하는 경우) ProviderScope를 사용해야함
testWidgets(
'account screen ...',
(tester) async {
await tester.pumpWidget(
const ProviderScope(
child: MaterialApp(
home: AccountScreen(),
),
),
);
},
);
텍스트를 찾아주는 기능
final finder = find.text("___");
expect(finder, findsOneWidget);
버튼을 누른 후 “Are you sure?”이라는 텍스트가 있는지 확인
final finder = find.text("Logout");
expect(finder, findsOneWidget);
await tester.tap(finder);
await tester.pump(); // go to the next step
final dialogTitle = find.text('Are you sure?');
expect(dialogTitle, findsOneWidget);
위젯이 존재하지 않음을 체크하고 싶다면?
expect(dialogTitle, findsNothing);
: 사용자의 인터렉션을 모방하여 테스트하는 기법
예시) AuthRobot 클래스를 구현
class AuthRobot {
AuthRobot(this.tester);
final WidgetTester tester;
}
주의 사항: 하단의 메소드는 tester가 사용할 이유가 없기 때문에 future 값을 리턴하지 않음.
// This method does not return an Future because it doesn't use the tester
void expectLogoutDialogFound() {
final dialogTitle = find.text('Are you sure?');
expect(dialogTitle, findsOneWidget);
}
목적: 같은 이름을 가진 위젯을 구별하기 위함
특정 위젯을 가리키는 키를 정의
final finder = find.byKey(kDialogDefaultKey);
특정 위젯에 대한 타입도 처리 가능
final finder = find.byType(ElevatedButton);
delay가 있는지 체크
when(mockAuthRepository.signOut).thenAnswer(
(_) => Future.delayed(const Duration(seconds: 1)),
);
indicator 타입 체크
void expectCircularProgressIndicator() {
final finder = find.byType(CircularProgressIndicator);
expect(finder, findsOneWidgets);
}
async method 작성
runAsync를 사용하는 이유: Future.delayed 타이머는 내부적으로 이루어지는데, 이러한 이유 떄문에 타이머가 끝나지 않은 상태로 테스트가 종료될 수 있음. (비동기 호출을 처리할 수 있는 충분한 시간을 테스트 코드에 부여해야함)
await tester.runAsync(() async {
await r.tapLogoutButton();
r.expectLogoutDialogFound();
await r.tapDialogLogoutButton();
});
테스트 코드 정의
import 'package:ecommerce_app/src/features/authentication/auth_robot.dart';
import 'package:ecommerce_app/src/features/authentication/presentation/sign_in/email_password_sign_in_state.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import '../../../../mocks.dart';
void main() {
const testEmail = 'test@test.com';
const testPassword = '1234';
late MockAuthRepository authRepository;
setUp(() {
authRepository = MockAuthRepository();
});
group('sign in', () {
testWidgets('''
Given formType is signIn
When tap on the sign-in button
Then signInWithEmailAndPassword is not called
''', (tester) async {
final r = AuthRobot(tester);
await r.pumpEmailPasswordSignInContents(
authRepository: authRepository,
formType: EmailPasswordSignInFormType.signIn,
);
// signIn WithEmailAndPassword(): 이메일과 비밀번호가 비어있으면 호출되지 않음
await r.tapEmailAndPasswordSubmitButton();
verifyNever(() => authRepository.signInWithEmailAndPassword(
any(), // a method is not called with any arguments
any(),
));
});
});
}
pumpWidget 메소드 정의
Future<void> pumpEmailPasswordSignInContents(
{required FakeAuthRepository authRepository,
required EmailPasswordSignInFormType formType,
VoidCallback? onSignIn}) {
return tester.pumpWidget(
ProviderScope(
overrides: [
authRepositoryProvider.overrideWithProvider(authRepository)
],
child: MaterialApp(
home: Scaffold(
body: EmailPasswordSignInContents(
formType: formType,
onSignedIn: onSignIn,
),
),
),
),
);
}
submit 버튼 클릭 액션 정의
Future<void> tapEmailAndPasswordSubmitButton() async {
final primaryButton = find.byType(PrimaryButton);
expect(primaryButton, findsOneWidget);
await tester.tap(primaryButton);
await tester.pump();
}
위젯이 정상 작동하는지 체크
testWidgets('callback test', (tester) async {
// flag to keep track if the callback was called
var didSignIn = false;
final r = AuthRobot(tester);
// TODO: stub mock
await r.pumpEmailPasswordSignInContents(
authRepository: authRepository,
formType: EmailPasswordSignInFormType.signIn,
// set flag to true when callback is called
onSignedIn: () => didSignIn = true,
);
await r.enterEmail(testEmail);
await r.enterPassword(testPassword);
await r.tapEmailAndPasswordSubmitButton();
verify(() => authRepository.signInWithEmailAndPassword(
testEmail,
testPassword,
)).called(1);
r.expectErrorAlertNotFound();
// expect that callback was called
expect(didSignIn, true);
});
});
예시
test('Golden test', (tester) async {
final r = Robot(tester);
await r.pumpMyApp();
await expectLater(
find.byType(MyApp),
matchesGoldenFile('product_list.png',)
);
});
Golden Image Test 수행 방법
golden image 생성
flutter test --update-goldens
태그 옵션을 달면, 태그에 대한 테스트만 처리 가능
flutter test --update-goldens --tags=golden
아니면 dart_test.yaml파일을 생성하여 tag 리스트를 만드는 방법도 존재
// dart_test.yaml
tags:
golden:
테스트
flutter test
만약 아웃풋 이미지가 골든 이미지와 매치되지 않는다면, 테스트는 실패될 것이고, 비교에 사용할 이미지가 잘못 출력되어 생성될 수 있음.
size variant 정의
final sizeVariant = ValueVariant<Size>({
const Size(300, 600),
const Size(600, 800),
const Size(1000, 1000),
})
현재 사이즈 체크
final currentSize = sizeVariant.currentValue!:
await r.golden.setSurfaceSize(currentSize);
테스트에 size variant 추가
test('Golden test', (tester) async {
final r = Robot(tester);
await r.pumpMyApp();
await expectLater(
find.byType(MyApp),
matchesGoldenFile('product_list.png',)
);
} variant: sizeVariant,
tags: ['golden'] // optional
);