[Flutter] Automated Testing 2

yeahsilver·2023년 6월 22일
0
post-thumbnail

Widget Test

: 각 페이지 또는 위젯을 테스트하는 방식

pumpWidget

: 주어진 위젯을 통해 UI 렌더링

ProviderScope

: provider의 state가 저장되어있는 위젯

ConsumerWidget와 같이 Ref가 존재하는 경우 (= provider를 사용하는 경우) ProviderScope를 사용해야함

testWidgets(
    'account screen ...',
    (tester) async {
      await tester.pumpWidget(
        const ProviderScope(
          child: MaterialApp(
            home: AccountScreen(),
          ),
        ),
      );
    },
  );

Finder

텍스트를 찾아주는 기능

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);

로봇 테스트

: 사용자의 인터렉션을 모방하여 테스트하는 기법

  • 방법 1: 각 테스트 파일별로 로봇 클래스 생성
  • 방법 2: 각 가능별로 로봇 클래스 생성

구현 순서

예시) 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);
  }

Key를 이용한 위젯 검색 방법

목적: 같은 이름을 가진 위젯을 구별하기 위함

특정 위젯을 가리키는 키를 정의

final finder = find.byKey(kDialogDefaultKey);

특정 위젯에 대한 타입도 처리 가능

final finder = find.byType(ElevatedButton);

Future.delayed와 runAsync()를 사용한 위젯 테스트 작성

  1. delay가 있는지 체크

    when(mockAuthRepository.signOut).thenAnswer(
          (_) => Future.delayed(const Duration(seconds: 1)),
        );
  1. indicator 타입 체크

    void expectCircularProgressIndicator() {
    	final finder = find.byType(CircularProgressIndicator);
    	expect(finder, findsOneWidgets);
    }
  1. async method 작성

    runAsync를 사용하는 이유: Future.delayed 타이머는 내부적으로 이루어지는데, 이러한 이유 떄문에 타이머가 끝나지 않은 상태로 테스트가 종료될 수 있음. (비동기 호출을 처리할 수 있는 충분한 시간을 테스트 코드에 부여해야함)

    await tester.runAsync(() async {
      await r.tapLogoutButton();
      r.expectLogoutDialogFound();
      await r.tapDialogLogoutButton();
    });

email & password widget 테스트 추가

  1. 테스트 코드 정의

    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(),
              ));
        });
      });
    }
  1. 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,
                ),
              ),
            ),
          ),
        );
      }
  1. submit 버튼 클릭 액션 정의

    Future<void> tapEmailAndPasswordSubmitButton() async {
        final primaryButton = find.byType(PrimaryButton);
        expect(primaryButton, findsOneWidget);
        await tester.tap(primaryButton);
        await tester.pump();
      }
  1. 위젯이 정상 작동하는지 체크

    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);
        });
      });

Golden Image Test

  • 특정 스크린 또는 위젯이 golden image와 동일한지 검증
  • UI regression 체크에 용이함
  • 주로 custom UI 및 반응형 앱을 체크할 때 사용

예시

test('Golden test', (tester) async {
	final r = Robot(tester);
	await r.pumpMyApp();
	await expectLater(
		find.byType(MyApp),
		matchesGoldenFile('product_list.png',)
	);
});

Golden Image Test 수행 방법

  1. golden image 생성

    flutter test --update-goldens

태그 옵션을 달면, 태그에 대한 테스트만 처리 가능

flutter test --update-goldens --tags=golden

아니면 dart_test.yaml파일을 생성하여 tag 리스트를 만드는 방법도 존재

// dart_test.yaml
tags:
    golden:
  1. 테스트

    flutter test

만약 아웃풋 이미지가 골든 이미지와 매치되지 않는다면, 테스트는 실패될 것이고, 비교에 사용할 이미지가 잘못 출력되어 생성될 수 있음.

Size Variant를 이용한 Golden image test

  1. size variant 정의

    final sizeVariant = ValueVariant<Size>({
    	const Size(300, 600),
    	const Size(600, 800),
    	const Size(1000, 1000),
    })
  1. 현재 사이즈 체크

    final currentSize = sizeVariant.currentValue!:
    await r.golden.setSurfaceSize(currentSize);
  1. 테스트에 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
    );
profile
Hello yeahsilver's world!

0개의 댓글