티스토리 뷰
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 앱의 테스트 디렉토리의 예제들을 참고하거나 플러터의 테스트 문서를 읽어보자.
'Flutter > 아키텍처' 카테고리의 다른 글
플러터 앱 아키텍처 _ 6. 아키텍처 예제 _ 계층간 통신(의존성 주입) (0) | 2025.04.15 |
---|---|
플러터 앱 아키텍처 _ 5. 아키텍처 예제 _ 데이터 계층 (0) | 2025.04.15 |
플러터 앱 아키텍처 _ 4. 아키텍처 예제 _ UI 계층 (0) | 2025.04.15 |
플러터 앱 아키텍처 _ 3. 아키텍처 예제 _ 개요 (0) | 2025.04.15 |
플러터 앱 아키텍처 _ 2. 앱 아키텍처 가이드 (0) | 2025.04.15 |