티스토리 뷰

반응형

UI 계층 테스트하기

아키텍처가 잘 설계되었는지를 판단하는 한 가지 방법은 테스트하기가 얼마나 쉬운지를 보는 것이다. 뷰모델과 뷰는 입력값이 명확하게 정의되어 있기 때문에 그 의존성을 쉽게 모킹(mock)하거나 페이킹(fake)할 수 있으며, 유닛 테스트도 쉽게 작성할 수 있다.


 

ViewModel 유닛 테스트

뷰모델의 유일한 의존성은 리포지토리(또는 유즈케이스)이며, 리포지토리를 모킹하거나 페이킹하는 것이 유일한 사전 작업이다.

 

다음 테스트 예제에서는 FakeBookingRepository라는 페이크 객체를 사용한다.

// home_screen_test.dart
void main() {
  group('HomeViewModel tests', () {
    test('Load bookings', () {
      // HomeViewModel._load는 생성자에서 호출된다.
      final viewModel = HomeViewModel(
        bookingRepository: FakeBookingRepository()
          ..createBooking(kBooking),
        userRepository: FakeUserRepository(),
      );

      expect(viewModel.bookings.isNotEmpty, true);
    });
  });
}
// fake_booking_repository.dart
class FakeBookingRepository implements BookingRepository {
  List<Booking> bookings = List.empty(growable: true);

  @override
  Future<Result<void>> createBooking(Booking booking) async {
    bookings.add(booking);
    return Result.ok(null);
  }
  // ...
}

 

만약 유즈케이스를 함께 사용하는 경우, 유즈케이스도 페이크로 대체해야 한다.


 

View 위젯 테스트

뷰모델에 대한 테스트를 작성했다면, 위젯 테스트를 작성하는 데 필요한 페이크 객체들도 이미 만들어진 셈이다. 다음 예제는 HomeScreen 위젯 테스트가 어떻게 HomeViewModel과 필요한 리포지토리들을 사용하는지를 보여준다.

// home_screen_test.dart
void main() {
  group('HomeScreen tests', () {
    late HomeViewModel viewModel;
    late MockGoRouter goRouter;
    late FakeBookingRepository bookingRepository;

    setUp(() {
      bookingRepository = FakeBookingRepository()
        ..createBooking(kBooking);
      viewModel = HomeViewModel(
        bookingRepository: bookingRepository,
        userRepository: FakeUserRepository(),
      );
      goRouter = MockGoRouter();
      when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null));
    });

    // ...
  });
}

 

이 설정은 필요한 두 개의 페이크 리포지토리를 생성하고 그것들을 HomeViewModel 객체에 주입한다. 여기서 MockGoRouter는 package:mocktail을 이용해서 모킹하였다.

 

뷰모델과 그 의존성을 정의한 후에는 테스트할 위젯 트리를 생성해야 한다. 여기서는 loadWidget이라는 메서드를 정의했다.

// home_screen_test.dart
void main() {
  group('HomeScreen tests', () {
    late HomeViewModel viewModel;
    late MockGoRouter goRouter;
    late FakeBookingRepository bookingRepository;

    setUp(
      // ...
    );

    void loadWidget(WidgetTester tester) async {
      await testApp(
        tester,
        ChangeNotifierProvider.value(
          value: FakeAuthRepository() as AuthRepository,
          child: Provider.value(
            value: FakeItineraryConfigRepository() as ItineraryConfigRepository,
            child: HomeScreen(viewModel: viewModel),
          ),
        ),
        goRouter: goRouter,
      );
    }

    // ...
  });
}

 

이 메서드는 testApp이라는 일반화된 메서드를 호출하는데, Compass 앱에서 모든 위젯 테스트에 사용된다. testApp은 다음과 같다.

// testing/app.dart
void testApp(
  WidgetTester tester,
  Widget body, {
  GoRouter? goRouter,
}) async {
  tester.view.devicePixelRatio = 1.0;
  await tester.binding.setSurfaceSize(const Size(1200, 800));
  await mockNetworkImages(() async {
    await tester.pumpWidget(
      MaterialApp(
        localizationsDelegates: [
          GlobalWidgetsLocalizations.delegate,
          GlobalMaterialLocalizations.delegate,
          AppLocalizationDelegate(),
        ],
        theme: AppTheme.lightTheme,
        home: InheritedGoRouter(
          goRouter: goRouter ?? MockGoRouter(),
          child: Scaffold(
            body: body,
          ),
        ),
      ),
    );
  });
}

 

이 함수의 유일한 목적은 테스트 가능한 위젯 트리를 생성하는 것이다.

 

loadWidget 메서드는 위젯 트리의 고유한 부분들을 테스트를 위해 주입한다. 이 경우 HomeScreen과 뷰모델, 페이크 리포지토리들이 포함된다. 중요한 것은 아키텍처가 잘 설계되어 있다면 뷰 및 뷰모델 테스트는 리포지토리만 모킹하면 된다는 점이다.


 

데이터 계층 테스트

UI 계층과 마찬가지로, 데이터 계층의 컴포넌트들도 명확한 입력과 출력을 가지므로, 양쪽 모두 페이킹이 가능하다. 특정 리포지토리에 대한 유닛 테스트를 작성하려면, 그 리포지토리가 의존하는 서비스들을 모킹하면 된다. 

 

다음은 BookingRepository에 대한 유닛 테스트 예제이다.

// booking_repository_remote_test.dart
void main() {
  group('BookingRepositoryRemote tests', () {
    late BookingRepository bookingRepository;
    late FakeApiClient fakeApiClient;

    setUp(() {
      fakeApiClient = FakeApiClient();
      bookingRepository = BookingRepositoryRemote(
        apiClient: fakeApiClient,
      );
    });

    test('should get booking', () async {
      final result = await bookingRepository.getBooking(0);
      final booking = result.asOk.value;
      expect(booking, kBooking);
    });
  });
}

 

모킹 및 페이킹에 대해 더 알고 싶다면 Compass 앱의 테스트 디렉토리의 예제들을 참고하거나 플러터의 테스트 문서를 읽어보자.

반응형
댓글
공지사항