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(); } }
问题原因与解决方案
原因分析
- Mock的notifyListeners无实际通知逻辑:使用
Mock实现TextFProvider时,将notifyListeners()设置为空实现,而Consumer依赖ChangeNotifier的通知机制触发重建,空实现无法通知任何监听者。 - 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
相关产品推荐
相关产品推荐

