跳到主内容

测试每一层

如何测试实现了 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 测试文档

反馈

#

随着本网站内容的持续更新,我们欢迎您的反馈