跳转至正文

测试各层

如何测试实现 MVVM 架构的应用。

测试 UI 层

#

判断架构是否合理的一种方式,是看应用是否易于测试。由于 view model 与 view 输入明确,依赖易于 mock 或 fake,单元测试也易编写。

ViewModel 单元测试

#

测试 view model 的 UI 逻辑时,应编写不依赖 Flutter 库或测试框架的单元测试。

仓库是 view model 的唯一依赖(除非实现 用例),只需为仓库编写 mockfake。本示例测试使用名为 FakeBookingRepository 的 fake。

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

View widget 测试

#

为 view model 写好测试后,编写 widget 测试所需的 fake 也已就绪。以下示例展示如何使用 HomeViewModel 与所需仓库设置 HomeScreen widget 测试:

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));
    });

    // ...
  });
}

该设置创建两个 fake 仓库并传入 HomeViewModel,该类无需 fake。

定义 view model 及其依赖后,需创建待测 Widget 树。 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,
      );
    }

    // ...
  });
}

该方法转而调用 Compass 应用所有 widget 测试共用的 testApp,如下:

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,
          ),
        ),
      ),
    );
  });
}

该函数唯一职责是创建可测试的 widget 树。

loadWidget 传入测试中 widget 树的独特部分,包括 HomeScreen 及其 view model,以及 widget 树更高处的额外 fake 仓库。

最重要的是:若架构合理,view 与 view model 测试只需 mock 仓库。

测试数据层

#

与 UI 层类似,数据层组件输入输出明确,两侧都可 fake。为任意仓库编写单元测试时,mock 其依赖的 service。以下示例为 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 应用 testing 目录Flutter 测试文档

反馈

#

网站本节内容仍在完善中, 欢迎提供反馈