测试每一层
测试 UI 层
#判断架构是否健全的一种方法是考虑应用程序的测试难易程度。由于视图模型和视图具有明确定义的输入,因此它们的依赖项可以轻松地被模拟或伪造,并且单元测试很容易编写。
视图模型单元测试
#为了测试视图模型的 UI 逻辑,您应该编写不依赖于 Flutter 库或测试框架的单元测试。
仓库是视图模型唯一的依赖项(除非您正在实现用例),编写仓库的 mocks
或 fakes
是您唯一需要做的设置。在这个示例测试中,使用了名为 FakeBookingRepository
的伪造对象。
void main() {
group('HomeViewModel tests', () {
test('Load bookings', () {
// HomeViewModel._load 在 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);
}
// ...
}
视图 Widget 测试
#一旦您为视图模型编写了测试,您就已经创建了编写 Widget 测试所需的伪造对象。以下示例显示了如何使用 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));
});
// ...
});
}
此设置创建了所需的两个伪造仓库,并将它们传递到 HomeViewModel
对象中。此类不需要被伪造。
定义视图模型及其依赖项后,需要创建将要测试的 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 应用中所有 Widget 测试中使用的通用方法。它看起来像这样:
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
及其视图模型,以及 Widget 树中更高一些的其他伪造仓库。
最重要的收获是,如果您的架构健全,视图和视图模型测试只需要模拟仓库。
测试数据层
#与 UI 层类似,数据层的组件具有明确定义的输入和输出,使得双方都可以伪造。要为任何给定的仓库编写单元测试,请模拟它所依赖的服务。以下示例显示了 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);
});
});
}
要了解有关编写模拟和伪造对象的更多信息,请查看Compass App testing
目录中的示例,或阅读Flutter 的测试文档。
反馈
#由于本网站的这一部分正在不断发展,我们欢迎您的反馈!
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。