測試每一層
如何測試實現 MVVM 架構的應用程式。
測試 UI 層
#確定你的架構是否合理的一種方法是考慮應用程式的可測試性(或難度)。由於 ViewModel 和 View 具有明確定義的輸入,因此它們的依賴項可以輕鬆地被模擬或偽造,並且可以輕鬆編寫單元測試。
ViewModel 單元測試
#為了測試 ViewModel 的 UI 邏輯,你應該編寫不依賴於 Flutter 庫或測試框架的單元測試。
Repository 是 ViewModel 的唯一依賴項(除非你正在實現 用例),編寫 Repository 的 mocks 或 fakes 是你唯一需要做的設定。在這個示例測試中,使用了名為 FakeBookingRepository 的假物件。
void main() {
group('HomeViewModel tests', () {
test('Load bookings', () {
// HomeViewModel._load is called in the constructor of HomeViewModel.
final viewModel = HomeViewModel(
bookingRepository: FakeBookingRepository()
..createBooking(kBooking),
userRepository: FakeUserRepository(),
);
expect(viewModel.bookings.isNotEmpty, true);
});
});
}
FakeBookingRepository 類實現了 BookingRepository。 在本案例研究的 資料層部分,BookingRepository 類得到了徹底的解釋。
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 元件測試
#一旦你為 ViewModel 編寫了測試,你就已經建立了編寫元件測試所需的假物件。 以下示例展示瞭如何使用 HomeViewModel 和所需的 Repository 設定 HomeScreen 元件測試
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));
});
// ...
});
}
此設定建立了兩個所需的假 Repository,並將它們傳遞到 HomeViewModel 物件中。 此類不需要被偽造。
在定義了 ViewModel 及其依賴項之後,需要建立將要測試的 Widget 樹。 在 HomeScreen 的測試中,定義了一個 loadWidget 方法。
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 應用中所有元件測試的通用方法。 它看起來像這樣
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,
),
),
),
);
});
}
此函式的唯一作用是建立一個可以進行測試的 Widget 樹。
loadWidget 方法傳遞了 Widget 樹的唯一部分以進行測試。 在這種情況下,包括 HomeScreen 及其 ViewModel,以及位於 Widget 樹中更高位置的一些額外的偽造 Repository。
最重要的是,如果你的架構合理,View 和 ViewModel 測試只需要模擬 Repository。
測試資料層
#與 UI 層類似,資料層的元件具有明確定義的輸入和輸出,使得雙方都可以被偽造。 要為任何給定的 Repository 編寫單元測試,請模擬它所依賴的服務。 以下示例展示了 BookingRepository 的單元測試。
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);
});
});
}
要了解更多關於編寫 mocks 和 fakes 的資訊,請檢視 Compass App testing 目錄 中的示例,或閱讀 Flutter 的測試文件。
反饋
#由於本網站的這一部分正在不斷發展,我們 歡迎你的反饋!