如何使用Bloc Test测试异步Future函数及模拟定位请求
Bloc测试问题:异步状态验证与定位API模拟
我是Bloc新手,正在学习Bloc测试。以下GoogleMapBloc代码在调试模式运行正常,但测试异步获取当前位置的函数时,不清楚如何无需使用wait等待来验证状态/错误。此外,我还想了解如何模拟该定位API请求。
我刚接触Bloc,目前正在学习Bloc测试。以下代码在调试模式下运行正常,但我找不到合适的方式等待异步函数完成,以测试当前位置或错误状态。这是我的Bloc代码:
class GoogleMapBloc extends Bloc<GoogleMapEvent, GoogleMapState> { GoogleMapBloc() : super(const GoogleMapState()) { on<CurrentLocationEvent>(_showCurrentPosition); } //Bloc核心逻辑 void _showCurrentPosition( CurrentLocationEvent event, Emitter<GoogleMapState> emit) async { try { emit(state.copyWith(loadStatus: LoadStatus.loading)); Position position = await _getCurrentLocation(); final Marker marker = Marker( markerId: const MarkerId('current_position'), position: LatLng(position.latitude, position.longitude), ); Set<Marker> markers = {}; markers.add(marker); emit(state.copyWith( loadStatus: LoadStatus.success, currentPosition: position, markers: markers)); } catch (e) { emit(state.copyWith( loadStatus: LoadStatus.fail, markers: {}, error: e.toString().substring(11))); } } //获取当前位置或抛出错误 Future<Position> _getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; //检查定位服务是否启用 serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { throw Exception('Location services are disabled.'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { throw Exception('Location permissions are denied'); } } if (permission == LocationPermission.deniedForever) { throw Exception( 'Location permissions are permanently denied, we cannot request permissions.'); } //权限通过,返回当前位置 return await Geolocator.getCurrentPosition(); } }
我困惑的是,这是异步调用,如何不用wait来等待函数完成以测试状态?
void googleMapBloc() { group("Google Map Bloc Testing", () { blocTest<GoogleMapBloc, GoogleMapState>( 'Check location enabled', build: () => GoogleMapBloc(), act: (bloc) => bloc.add(const CurrentLocationEvent()), wait: const Duration(milliseconds: 10000), expect: () => <GoogleMapState>[ const GoogleMapState(loadStatus: LoadStatus.loading), const GoogleMapState( markers: {}, loadStatus: LoadStatus.fail, error: ""), ], ); }); }
另外,我该如何模拟这个API请求?
一、无需wait验证异步状态
blocTest本身已经内置了异步状态的等待逻辑,不需要手动设置wait的核心是让Bloc的异步操作变得可控——也就是通过模拟定位服务,把不可预测的真实API请求替换成同步或可预期的异步操作。
当你使用模拟服务后,_showCurrentPosition中的异步逻辑会变成可预测的流程,blocTest会自动等待Bloc发射完所有状态,无需手动设置等待时长。
调整后的测试示例(配合下文的模拟服务):
void googleMapBloc() { group("Google Map Bloc Testing", () { blocTest<GoogleMapBloc, GoogleMapState>( '获取当前位置成功', build: () => GoogleMapBloc(locationService: MockSuccessLocationService()), act: (bloc) => bloc.add(const CurrentLocationEvent()), expect: () => <GoogleMapState>[ const GoogleMapState(loadStatus: LoadStatus.loading), GoogleMapState( loadStatus: LoadStatus.success, currentPosition: Position( latitude: 37.7749, longitude: -122.4194, timestamp: any, accuracy: 10, altitude: 0, heading: 0, speed: 0, speedAccuracy: 0, ), markers: { Marker( markerId: const MarkerId('current_position'), position: const LatLng(37.7749, -122.4194), ) }, ), ], ); blocTest<GoogleMapBloc, GoogleMapState>( '定位服务禁用时返回错误', build: () => GoogleMapBloc(locationService: MockDisabledLocationService()), act: (bloc) => bloc.add(const CurrentLocationEvent()), expect: () => <GoogleMapState>[ const GoogleMapState(loadStatus: LoadStatus.loading), const GoogleMapState( loadStatus: LoadStatus.fail, markers: {}, error: 'Location services are disabled.', ), ], ); }); }
二、模拟定位API请求
模拟Geolocator的核心是依赖注入——把定位逻辑抽象成接口,让Bloc依赖接口而非具体的Geolocator实现,测试时替换成模拟的接口实现即可。
1. 抽象定位服务接口
abstract class LocationService { Future<bool> isLocationServiceEnabled(); Future<LocationPermission> checkPermission(); Future<LocationPermission> requestPermission(); Future<Position> getCurrentPosition(); }
2. 实现生产环境的真实服务
class RealLocationService implements LocationService { @override Future<bool> isLocationServiceEnabled() => Geolocator.isLocationServiceEnabled(); @override Future<LocationPermission> checkPermission() => Geolocator.checkPermission(); @override Future<LocationPermission> requestPermission() => Geolocator.requestPermission(); @override Future<Position> getCurrentPosition() => Geolocator.getCurrentPosition(); }
3. 修改Bloc依赖接口
class GoogleMapBloc extends Bloc<GoogleMapEvent, GoogleMapState> { final LocationService locationService; // 通过构造函数注入服务 GoogleMapBloc({required this.locationService}) : super(const GoogleMapState()) { on<CurrentLocationEvent>(_showCurrentPosition); } // 生产环境默认构造 GoogleMapBloc.default() : this(locationService: RealLocationService()); void _showCurrentPosition( CurrentLocationEvent event, Emitter<GoogleMapState> emit) async { try { emit(state.copyWith(loadStatus: LoadStatus.loading)); // 使用注入的服务,而非直接调用Geolocator bool serviceEnabled = await locationService.isLocationServiceEnabled(); if (!serviceEnabled) { throw Exception('Location services are disabled.'); } LocationPermission permission = await locationService.checkPermission(); if (permission == LocationPermission.denied) { permission = await locationService.requestPermission(); if (permission == LocationPermission.denied) { throw Exception('Location permissions are denied'); } } if (permission == LocationPermission.deniedForever) { throw Exception( 'Location permissions are permanently denied, we cannot request permissions.'); } Position position = await locationService.getCurrentPosition(); final Marker marker = Marker( markerId: const MarkerId('current_position'), position: LatLng(position.latitude, position.longitude), ); Set<Marker> markers = {}; markers.add(marker); emit(state.copyWith( loadStatus: LoadStatus.success, currentPosition: position, markers: markers)); } catch (e) { emit(state.copyWith( loadStatus: LoadStatus.fail, markers: {}, error: e.toString().substring(11))); } } }
4. 编写模拟服务用于测试
可以手动编写模拟类,也可以用mockito库生成更灵活的模拟:
手动模拟成功场景
class MockSuccessLocationService implements LocationService { @override Future<bool> isLocationServiceEnabled() async => true; @override Future<LocationPermission> checkPermission() async => LocationPermission.always; @override Future<LocationPermission> requestPermission() async => LocationPermission.always; @override Future<Position> getCurrentPosition() async => Position( latitude: 37.7749, longitude: -122.4194, timestamp: DateTime.now(), accuracy: 10, altitude: 0, heading: 0, speed: 0, speedAccuracy: 0, ); }
手动模拟服务禁用场景
class MockDisabledLocationService implements LocationService { @override Future<bool> isLocationServiceEnabled() async => false; @override Future<LocationPermission> checkPermission() async => LocationPermission.denied; @override Future<LocationPermission> requestPermission() async => LocationPermission.denied; @override Future<Position> getCurrentPosition() async => throw UnimplementedError(); }
核心要点
- 依赖注入让Bloc与外部服务解耦,测试时可以完全控制输入输出
- 模拟后的异步操作是可预期的,
blocTest会自动等待状态发射完成,无需手动设置wait - 使用
mockito可以更灵活地配置方法的返回值或抛出异常,减少手动编写模拟类的工作量
内容的提问来源于stack exchange,提问作者Akshay Rangari
相关产品推荐
相关产品推荐

