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

Flutter Dio单元测试报错:'Null'不是'Interceptors'的子类型

Flutter Dio 单元测试报错:type 'Null' is not a subtype of type 'Interceptors'

问题背景

开发Flutter应用时,使用Dio处理网络请求,为封装在AppClient中的Dio post方法编写单元测试时遇到类型不匹配错误。

核心代码

app_client.dart

abstract class AppClient{
  Future<Map<String, dynamic>> get(String path, {Map<String, String>? queryParameters, Map<String, String>? headers});
  Future<Map<String, dynamic>> post(String path, dynamic data, {Map<String, String>? queryParameters, Map<String, String>? headers});
}

class AppClientImpl implements AppClient{

  late Dio dio;
  late Interceptors interceptors;

  AppClientImpl({required this.dio}){
    //获取语言编码
    String languageCode = StorageUtil.getSavedLanguage();
    if(languageCode == "id-ID"){ 
      languageCode = "id_ID";
    }else{
      languageCode = "en_US";
    }

    BaseOptions baseOptions = BaseOptions(
        baseUrl: ServiceUrl.baseUrl,
        headers: {
          "Accept" : "application/json",
          "Content-Type" : "application/json",
        },
        queryParameters: {
          "language" : languageCode,
          "channel" : "mobile"
        }
    );

    dio.options = baseOptions;

    interceptors = Interceptors();
    interceptors.add(LogInterceptor(request: true, requestBody: true, requestHeader: true, responseBody: true, responseHeader: true));

    //测试时注释仍报错
    dio.interceptors.addAll(interceptors); // 错误发生位置!!!
  }

  @override
  Future<Map<String, dynamic>> get(String path, {Map<String, String>? queryParameters, Map<String, String>? headers}) async {
    // return await dio.get(path, queryParameters: queryParameters);
    throw UnimplementedError();
  }

  Future<Map<String, dynamic>> post(String path, dynamic data, {Map<String, String>? queryParameters, Map<String, String>? headers}) async{
    Response response = await dio.post(path, queryParameters: queryParameters, data: Map<String, dynamic>.from(data));

    if(response.statusCode == 200){
      return jsonDecode(response.data);
    }else{
      throw Exception();
    }
  }

}

测试类代码

class MockDio extends Mock implements Dio{}

void main(){

  late MockDio mockDio;
  late AppClientImpl appClient;

  setUp((){
    mockDio = MockDio();
    appClient = AppClientImpl(dio: mockDio);
  });

  final tResponse = jsonDecode(fixture("token/token_success.json"));
  final tData = {};
  
  group("post method", (){
    test(
        "状态码200时应返回数据",
        ()async{
          when(
              () => mockDio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))
          ).thenAnswer(
              (invocation) async => Response(requestOptions: RequestOptions(path: "/sample"), data: fixture("token/token_success.json"), statusCode: 200)
          );

          final result = await appClient.post("/sample", tData);
          verify(() => mockDio.post(any(), queryParameters: any(named: "queryParameters"), data: any(named: "data"))).called(1);

          expect(result, tResponse);

        }
    );
  });

}

报错信息

Testing started at 08.54 ...

package:dio/src/dio.dart 46:20                                    MockDio.interceptors
package:eazyconnect/data/network/app_client/app_client.dart 44:9  new AppClientImpl
test/data/network/app_client/app_client_test.dart 23:17           main.<fn>

type 'Null' is not a subtype of type 'Interceptors'

错误原因

通过Mock生成的MockDio实例,所有未指定返回值的属性默认都会返回null,其中就包括interceptors属性。而在AppClientImpl的构造函数中执行dio.interceptors.addAll(interceptors)时,dio.interceptors返回的是null,但addAll方法要求接收一个Interceptors类型的实例,因此触发了类型不匹配的错误。

解决方法

方案1:为MockDio的interceptors指定返回值

在测试的setUp方法中,给mockDio的interceptors属性设置一个真实的Interceptors实例:

setUp((){
  mockDio = MockDio();
  // 为mockDio的interceptors赋值,避免返回null
  when(() => mockDio.interceptors).thenReturn(Interceptors());
  appClient = AppClientImpl(dio: mockDio);
});

方案2:重构AppClientImpl的构造逻辑

将拦截器的初始化逻辑改为可配置,允许外部传入自定义拦截器,测试时可以传入空拦截器:

// 修改AppClientImpl构造函数
class AppClientImpl implements AppClient{
  late Dio dio;
  late Interceptors interceptors;

  AppClientImpl({required this.dio, Interceptors? customInterceptors}){
    // ... 原有baseOptions逻辑
    
    // 使用传入的拦截器,无传入则创建新实例
    interceptors = customInterceptors ?? Interceptors();
    interceptors.add(LogInterceptor(request: true, requestBody: true, requestHeader: true, responseBody: true, responseHeader: true));
    dio.interceptors.addAll(interceptors);
  }
  
  // ... 其他方法不变
}

// 测试中使用
setUp((){
  mockDio = MockDio();
  when(() => mockDio.interceptors).thenReturn(Interceptors());
  // 传入空拦截器,避免LogInterceptor影响测试
  appClient = AppClientImpl(dio: mockDio, customInterceptors: Interceptors());
});

方案3:隔离静态依赖(额外优化)

测试中StorageUtil.getSavedLanguage()是静态方法,会导致测试依赖外部存储状态,建议封装为可注入的服务并Mock:

// 封装语言获取逻辑为可注入服务
abstract class LanguageService {
  String getSavedLanguage();
}

class LanguageServiceImpl implements LanguageService {
  @override
  String getSavedLanguage() => StorageUtil.getSavedLanguage();
}

// 修改AppClientImpl构造函数
AppClientImpl({required this.dio, required LanguageService languageService, Interceptors? customInterceptors}){
  String languageCode = languageService.getSavedLanguage();
  // ... 后续逻辑不变
}

// 测试中Mock LanguageService
class MockLanguageService extends Mock implements LanguageService{}

// 在setUp中使用
setUp((){
  mockDio = MockDio();
  when(() => mockDio.interceptors).thenReturn(Interceptors());
  final mockLanguageService = MockLanguageService();
  when(() => mockLanguageService.getSavedLanguage()).thenReturn("en_US");
  appClient = AppClientImpl(dio: mockDio, languageService: mockLanguageService);
});

内容的提问来源于stack exchange,提问作者fajar ainul

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 20:30:55