测试各层
如何测试实现 MVVM 架构的应用。
测试 UI 层
#判断架构是否合理的一种方式,是看应用是否易于测试。由于 view model 与 view 输入明确,依赖易于 mock 或 fake,单元测试也易编写。
ViewModel 单元测试
#测试 view model 的 UI 逻辑时,应编写不依赖 Flutter 库或测试框架的单元测试。
仓库是 view model 的唯一依赖(除非实现 用例),只需为仓库编写 mock 或 fake。本示例测试使用名为 FakeBookingRepository 的 fake。
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 widget 测试
#
为 view model 写好测试后,编写 widget 测试所需的 fake 也已就绪。以下示例展示如何使用 HomeViewModel 与所需仓库设置 HomeScreen widget 测试:
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 方法。
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,如下:
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 的单元测试。
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 测试文档。
反馈
#网站本节内容仍在完善中, 欢迎提供反馈!
除非另有说明,本文档之所提及适用于 Flutter 3.44.0 版本。本页面最后更新时间:2026-06-04。查看文档源码 或者 为本页面内容提出建议。