跳到主內容

測試每一層

如何測試實現了 MVVM 架構的應用程式。

測試 UI 層

#

判斷架構是否合理的途徑之一,是考慮應用程式的測試難度。由於檢視模型(ViewModel)和檢視(View)具有定義明確的輸入,因此它們的依賴項可以輕鬆地進行模擬(Mock)或偽造(Fake),從而使單元測試易於編寫。

ViewModel 單元測試

#

為了測試檢視模型的 UI 邏輯,你應該編寫不依賴於 Flutter 庫或測試框架的單元測試。

倉庫(Repositories)是檢視模型的唯一依賴項(除非你實現了用例),編寫倉庫的 mocksfakes 是你唯一需要做的配置工作。在此測試示例中,使用了名為 FakeBookingRepository 的偽造類。

home_screen_test.dart
dart
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 類進行了詳盡的解釋。

fake_booking_repository.dart
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);
  }
  // ...
}

檢視元件測試

#

一旦你為檢視模型編寫了測試,你也就建立了編寫元件測試所需的偽造類。以下示例展示瞭如何使用 HomeViewModel 和所需的倉庫來設定 HomeScreen 元件測試。

home_screen_test.dart
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 物件。該類本身不需要被偽造。

定義檢視模型及其依賴項後,需要建立將被測試的元件樹。在 HomeScreen 的測試中,定義了一個 loadWidget 方法。

home_screen_test.dart
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 應用中所有元件測試的通用方法。其實現如下:

testing/app.dart
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
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);
    });
  });
}

要了解更多關於編寫 Mock 和 Fake 的資訊,請檢視 Compass App testing 目錄中的示例,或閱讀 Flutter 測試文件

反饋

#

隨著本網站內容的持續更新,我們歡迎您的反饋