You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 11:35:22