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

Widget测试问题:Mock Provider的notifyListeners()未重建组件

TextField验证功能测试问题:Mock Provider触发notifyListeners后Consumer未重建

问题描述

我正在实现TextField的验证功能,需求为当输入字符长度大于3时启用按钮。在TextField的onChanged回调中,会调用Provider的notify()方法触发notifyListeners(),以此重建Widget。但在测试过程中,Mock的TextFProvider调用notifyListeners()后,Consumer并未重建Widget,导致测试无法通过。

测试文件代码

class MockTextFProvider extends Mock implements TextFProvider {}

void main() {
 late MockTextFProvider mockTextFProvider;

 setUp(() {
   mockTextFProvider = MockTextFProvider();
 });

 Future<void> pumpScreen(WidgetTester tester) async {
   return await tester.pumpWidget(
     MultiProvider(
       providers: [
         ChangeNotifierProvider<TextFProvider>.value(
           value: mockTextFProvider,
         ),
       ],
       child: const MaterialApp(
         home: TextFormF(),
       ),
     ),
   );
 }

 testWidgets('TextFormF empty value validation test',
     (WidgetTester tester) async {
   when(() => mockTextFProvider.notifyListeners()).thenAnswer((_) {});

   await pumpScreen(tester);

   // Find the TextFormField widget
   final textFieldFinder = find.byType(TextFormField);
   final bt = find.byType(ElevatedButton);

   expect(textFieldFinder, findsOneWidget);
   expect(bt, findsOneWidget);

   expect(tester.widget<ElevatedButton>(bt).onPressed, null);

   await tester.enterText(textFieldFinder, 'Test Value of textfield');
   await tester.pump();
  
   expect(tester.widget<ElevatedButton>(bt).onPressed, isNotNull);
 });
}

页面代码(使用Consumer)

class TextFormF extends StatefulWidget {
 const TextFormF({Key? key}) : super(key: key);

 @override
 State<TextFormF> createState() => _TextFormFState();
}

class _TextFormFState extends State<TextFormF> {
 final _formKey = GlobalKey<FormState>();

 String data = '';

 bool validate() {
   if (data.characters.length > 3) {
     return true;
   }

   return false;
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(),
     body: Consumer<TextFProvider>(builder: (context, provider, _) {
       return Form(
         key: _formKey,
         child: Column(
           children: [
             TextFormField(
               validator: (String? value) {
                 if (value != null && value.isEmpty) {
                   return "Empty value";
                 }
                 return null;
               },
               onChanged: (value) {
                 data = value!;
                 validate();

                 Provider.of<TextFProvider>(context, listen: false).notify();
               },
             ),
             ElevatedButton(
               onPressed: validate()
                   ? () {
                       if (_formKey.currentState!.validate()) {
                         ScaffoldMessenger.of(context).showSnackBar(
                           const SnackBar(content: Text('Processing Data')),
                         );
                       }
                     }
                   : null,
               child: const Text('Validate'),
             ),
           ],
         ),
       );
     }),
   );
 }
}

Provider类代码

class TextFProvider extends ChangeNotifier {
 void notify(){
   notifyListeners();
 }
}

问题原因与解决方案

原因分析

  1. Mock的notifyListeners无实际通知逻辑:使用Mock实现TextFProvider时,将notifyListeners()设置为空实现,而Consumer依赖ChangeNotifier的通知机制触发重建,空实现无法通知任何监听者。
  2. Mock未继承ChangeNotifier核心行为:Mock仅模拟了方法签名,没有继承ChangeNotifier的监听管理逻辑,无法正确触发Consumer的重建流程。

解决方案

方案1:使用Fake类模拟带真实通知逻辑的Provider

创建FakeTextFProvider,实现TextFProvider和ChangeNotifier的核心逻辑,确保notifyListeners()能正确通知监听者:

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/fake.dart';

class FakeTextFProvider extends Fake implements TextFProvider, ChangeNotifier {
  final _listeners = <VoidCallback>[];

  @override
  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  @override
  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }

  @override
  void notifyListeners() {
    for (final listener in _listeners) {
      listener();
    }
  }

  @override
  void notify() {
    notifyListeners();
  }
}

修改测试代码,替换Mock为Fake并调整重建等待逻辑:

void main() {
 late FakeTextFProvider fakeTextFProvider;

 setUp(() {
   fakeTextFProvider = FakeTextFProvider();
 });

 Future<void> pumpScreen(WidgetTester tester) async {
   return await tester.pumpWidget(
     MultiProvider(
       providers: [
         ChangeNotifierProvider<TextFProvider>.value(
           value: fakeTextFProvider,
         ),
       ],
       child: const MaterialApp(
         home: TextFormF(),
       ),
     ),
   );
 }

 testWidgets('TextFormF empty value validation test',
     (WidgetTester tester) async {
   await pumpScreen(tester);

   final textFieldFinder = find.byType(TextFormField);
   final bt = find.byType(ElevatedButton);

   expect(textFieldFinder, findsOneWidget);
   expect(bt, findsOneWidget);

   expect(tester.widget<ElevatedButton>(bt).onPressed, null);

   await tester.enterText(textFieldFinder, 'Test Value of textfield');
   await tester.pumpAndSettle(); // 确保Widget重建完成
  
   expect(tester.widget<ElevatedButton>(bt).onPressed, isNotNull);
 });
}

方案2:直接使用真实Provider实例测试

如果不需要验证notify()的调用次数,可直接使用真实的TextFProvider实例,无需模拟通知逻辑:

void main() {
 late TextFProvider textFProvider;

 setUp(() {
   textFProvider = TextFProvider();
 });

 // 后续测试代码与方案1一致,移除when(mockTextFProvider.notifyListeners())的设置
}

额外优化建议

将输入状态移到Provider中管理,让UI完全依赖Provider的状态,逻辑更清晰且易测试:

// 修改Provider类
class TextFProvider extends ChangeNotifier {
  String inputData = '';

  void updateInput(String value) {
    inputData = value;
    notifyListeners();
  }

  bool get isInputValid => inputData.length > 3;
}

// 修改页面代码
class _TextFormFState extends State<TextFormF> {
 final _formKey = GlobalKey<FormState>();

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(),
     body: Consumer<TextFProvider>(builder: (context, provider, _) {
       return Form(
         key: _formKey,
         child: Column(
           children: [
             TextFormField(
               validator: (String? value) {
                 if (value != null && value.isEmpty) {
                   return "Empty value";
                 }
                 return null;
               },
               onChanged: (value) {
                 provider.updateInput(value);
               },
             ),
             ElevatedButton(
               onPressed: provider.isInputValid
                   ? () {
                       if (_formKey.currentState!.validate()) {
                         ScaffoldMessenger.of(context).showSnackBar(
                           const SnackBar(content: Text('Processing Data')),
                         );
                       }
                     }
                   : null,
               child: const Text('Validate'),
             ),
           ],
         ),
       );
     }),
   );
 }
}

内容的提问来源于stack exchange,提问作者Shubham Hande

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 11:37:28