[Flutter]Navigation Test

한상욱·2025년 3월 2일
0

Flutter

목록 보기
30/30
post-thumbnail

들어가며

Flutter에서 테스트를 작성할 때 버튼 탭에 의하여 화면이 의도대로 이동하는지 확인하고 싶을때가 있습니다. 이러한 것은 통합 테스트 또는 위젯 테스트의 종류로 UI 테스트에서 사용하게 됩니다.

그 중에서 보통 개별의 위젯 테스트에서 사용할 정도의 라우팅 테스트에 대해서 알아봅시다.

Setup

테스트를 작성하기 위해서는 두가지 개념이 필요합니다. 바로, Mockito와 NavigatorObserver입니다.

NavigatorObserver는 Navigator의 동작을 관찰하기 위한 객체로 Navigation이 발생할 경우 검증 가능한 여러 메소드를 가지고 있습니다.

Mockito

Mockito는 테스트에 필요한 Mocking을 생성해주는 라이브러리로 단위 테스트에서 많이 사용됩니다. 하지만 저희는 Navigation을 위한 NavigatorObserver의 Mocking을 하기 위하여 사용할 것입니다.

사용할 위젯

현재 자취얌! 프로젝트에서는 통합 로그인 UI에서 회원가입 Text를 탭하면 회원가입 UI로 이동할 수 있어야 합니다.

저 하단의 회원가입이 오늘의 주인공입니다. UI코드도 하단에 볼 수 있습니다. 저는 테스트를 위하여 테스트가 필요한 위젯에 Key를 지정하였습니다.

	...
 Widget _resister() => SizedBox(
        width: 350,
        child: Builder(builder: (context) {
          final textTheme = Theme.of(context).textTheme.labelSmall;
          return Row(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              Text("자취얌이 처음이신가요?", style: textTheme),
              GestureDetector(
                /// 사용자가 위젯을 탭하면
                /// 회원가입 UI로 이동합니다.
                onTap: context.read<LoginViewModel>().moveToSignUp,
                child: Text(
                  key: const Key("login-view-resister-text-button"),
                  "회원 가입",
                  style: textTheme,
                ),
              )
            ],
          );
        }),
      );
      ...

라우팅 메소드

회원가입 Text는 LoginViewModel의 moveToSignUp() 메소드를 통해서 회원가입 UI로 이동합니다.

  //login_view_model.dart
  ...
  void moveToSignUp() {
    moveTo(const ResisterPage());
    notifyListeners();
  }

  void moveTo(Widget page) {
    SchedulerBinding.instance.addPostFrameCallback((_) {
      final context = GlobalVariable.naviagatorState.currentContext!;

      Navigator.of(context).push(MaterialPageRoute(builder: (context) => page));
    });
  }

moveTo()메소드는 사용하는 곳이 많아서 따로 메소드로 관리하고 있습니다. context가 없는 환경에서 context를 통해 navigation을 수행하기에 SchedulerBiding.instace.addPostFrameCallBack()을 이용합니다.

SchedulerBiding.instance.addPostFrameCallBack()을 왜 쓰냐면?

이 코드가 잘 이해안되는 분들을 위해서 잠시 설명을 하자면, 지금 context는 GlobalVariable 클래스 내부에서 선언하여 앱 상단에서 주입되어 현재의 context를 해당 static하게 관리하고 있습니다.

class GlobalVariable {
  static final GlobalKey<NavigatorState> naviagatorState =
      GlobalKey<NavigatorState>();
}

여기서 중요한 점은 context가 아직 없을수도 있다는 것이에요. 그래서

    SchedulerBinding.instance.addPostFrameCallback((_) {
      final context = GlobalVariable.naviagatorState.currentContext!;

      Navigator.of(context).push(MaterialPageRoute(builder: (context) => page));
    });

SchedulerBiding.instace.addPostFrameCallBack()을 통해서 프레임 렌더링이 완료된 후 예약을 거는것입니다. context의 null을 방지하는 일종의 방어책입니다.

Test 작성

Test에서는 두가지를 확인할 것입니다. 원하는 UI로 이동하는지, 그리고 push를 통해서 이동했는지를 확인합니다.

그 전에 테스트를 위한 패키지가 필요합니다. 이는 Mocking을 위해서 사용합니다. 저는 코드 제네레이션을 이용해서 Mocking을 할것이기에 build_runner도 추가하였습니다.

dev_dependencies:
  flutter_test:
    sdk: flutter
  ...
  mockito: ^5.4.4
  build_runner: ^2.4.13
  ...

이제 테스트를 작성해주겠습니다.

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:yum_application/src/core/utils/global_variable.dart';
import 'package:yum_application/src/ui/auth/page/login_page.dart';
import 'package:yum_application/src/ui/auth/view/resister_view.dart';

import 'login_view_test.mocks.dart';

([MockSpec<NavigatorObserver>()])
void main() {
  late final MaterialApp widget;
  late final MockNavigatorObserver observer;

  group("Login View UI 테스트", () {
    setUpAll(() {
      observer = MockNavigatorObserver();
      widget = MaterialApp(
        home: const LoginPage(),
        navigatorKey: GlobalVariable.naviagatorState,
        navigatorObservers: [observer],
      );
    });
  	...

    testWidgets("사용자는 이메일 로그인 버튼을 통해서 ResisterView로 이동할 수 있다.", (tester) async {
      await tester.pumpWidget(widget);
      await tester
          .tap(find.byKey(const Key("login-view-resister-text-button")));
      await tester.pumpAndSettle();
      expect(find.byType(ResisterView), findsOneWidget);
      verify(observer.didPush(any, any));
    });
  });
}

테스트를 작성할 때 주의점이 있습니다. 뷰모델에서는 context를 GlobalVariable에 등록된 navigatorState를 통해서 가져옵니다. 따라서, 실제 앱에서 선언된 것처럼 테스트 위젯에 주입해야 합니다.

그리고, 이러한 Navigator를 관찰하기 위한 MockNavigatorObserver를 생성해줍니다. 이는 Mocking된 NavigatorObserver로 라우팅 이벤트를 검증하기 위해서 사용됩니다.

주의점

class MockNavigatorObserver extends Mock implements NavigatorObserver {}

Mocking을 직접 선언하면 didPush는 직접 만들어야 됩니다.

단순하게 코드 제네레이션하는 것을 더 추천드립니다. 테스트에 많은 시간을 투자하는 것보다 간단하니까요.

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글

관련 채용 정보