Flutter使用Mockito测试时FutureBuilder Snapshot为null如何解决
问题解决方法
根因分析
你遇到的问题主要由以下几个错误导致:
- 测试执行逻辑错误:调用
tester.pumpWidget后直接执行断言,没有等待异步Future执行完成,此时Future处于pending状态,snapshot.hasData自然为false。 - FutureBuilder逻辑冗余:拿到异步快照数据后没有直接使用,反而额外将Future转为Stream监听赋值给实例变量,导致数据更新时序异常。
- 组件查找方式错误:使用
find.byWidget匹配组件,该方法要求组件实例完全相等,你测试中单独构造的ProductBox和页面渲染生成的ProductBox不是同一个实例,就算数据正确也无法匹配到。 - 组件状态规范错误:在
StatelessWidget中定义可变的实例变量存储列表数据,不符合无状态组件的设计规范,组件重绘时会导致数据丢失。
修复方案
1. 业务代码修改
调整FutureBuilder的逻辑,直接使用snapshot.data返回的结果,不要额外存储可变实例变量:
Expanded( child: FutureBuilder<List<ProductBoxModel>>( future: lastSearchFuture, builder: (context, snapshot){ // 先判断异步状态是否完成,再判断是否有数据 if(snapshot.connectionState == ConnectionState.done && snapshot.hasData){ final lastSearch = snapshot.data!; return Padding( padding: sidePadding, child: Container( width: size.width, height: size.height, child: ListView.builder( itemCount: lastSearch.length, itemBuilder: (context, index){ return ProductBox( productBoxModel: lastSearch[index], ); }, ), ) ); } else if (snapshot.hasError) { // 可选添加错误处理逻辑 return const Text('数据加载失败'); } else { return Container( alignment: Alignment.center, width: size.width, height: size.height, child: const CircularProgressIndicator(), ); } } ) ),
如果有同时等待多个Future的需求,可以使用Future.wait合并两个请求的Future,传给FutureBuilder即可。
2. 测试代码修改
首先补充缺失的queryParameters定义,修改组件查找逻辑,pumpWidget后添加等待异步任务完成的逻辑:
@GenerateMocks([http.Client]) void main(){ group('Landing Screen', (){ testWidgets('When get to server endpoint, then LandingScreen should shows ProductBox', (WidgetTester tester) async { await tester.runAsync(() async { final client = MockClient(); // 补充queryParameters定义,和接口请求参数保持一致 const queryParameters = {'userId': '5'}; final product = Product(id: "a1838sn18f", name: "Sabão em pó", description:"Tests",supplierItemCode: "35", unityMeasurement: "kg", unityMeasurementQuantity: 5, image:"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg" ); final supplier = Supplier(fantasyName: "aisdiaj", companyName: "TEste s1", CNPJ: "5575554"); // 修改组件查找方式,改为匹配类型,也可以根据组件属性比如商品名称查找 final findProductBox = find.byType(ProductBox); when(client .get(Uri.http(DOMAIN, LAST_SEARCH_URL, queryParameters))) .thenAnswer((_) async => http.Response('[{"product":{"id":"a1838sn18f","createdByUserId":"fake","createdDate":null","lastModifiedByUserId":null","lastModifiedDate":null","name":"Sabão em pó","description":"Tests","supplierItemCode":"35","unityMeasurement":"kg","unityMeasurementQuantity":5,"image":"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg"}' ',"supplier":{"id":"asdle2","createdByUserId":null,"createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"fantasyName":"aisdiaj","companyName":"TEste s1","cnpj":"5575554"}}]' , 200)); when(client .get(Uri.http(DOMAIN, MOST_SEARCH_URL, queryParameters))) .thenAnswer((_) async => http.Response('[{"product":{"id":"a1838sn18f","createdByUserId":"fake","createdDate":null","lastModifiedByUserId":null","lastModifiedDate":null","name":"Sabão em pó","description":"Tests","supplierItemCode":"35","unityMeasurement":"kg","unityMeasurementQuantity":5,"image":"https://a-static.mlcdn.com.br/1500x1500/sabao-em-pedra-ype-5-unidades-ype/costaatacado/90131/fd9f6b24567bc11702c189709a9f8dd3.jpg"}' ',"supplier":{"id":"asdle2","createdByUserId":null,"createdDate":null,"lastModifiedByUserId":null,"lastModifiedDate":null,"fantasyName":"aisdiaj","companyName":"TEste s1","cnpj":"5575554"}}]' , 200)); await tester.pumpWidget(MaterialApp(home: LandingScreen(client),)); // 等待所有异步任务执行完成、页面渲染结束 await tester.pumpAndSettle(); expect(findProductBox, findsWidgets); }); }); }); }
修改完成后即可正常等待接口返回数据后再执行断言,测试可以正常通过。
内容的提问来源于stack exchange,提问作者Vitor
相关产品推荐
相关产品推荐

