Riverpod 测试:如何使用 StateNotifierProvider 模拟状态?

Mat*_*out 8 testing mockito dart flutter

我的一些小部件具有条件 UI,可以根据状态显示/隐藏元素。我正在尝试设置根据状态(例如,用户角色)查找或不查找小部件的测试。我下面的代码示例被精简为一个小部件及其状态的基础知识,因为我似乎无法获得状态架构的最基本的实现来与模拟一起使用。

\n

当我遵循其他示例时,如下所示:

\n\n

我无法访问.state覆盖数组中的值。尝试运行测试时我还收到以下错误。这与无酒精鸡尾酒和无酒精鸡尾酒相同。我只能访问.notifier要覆盖的值(请参阅此处答案下的评论中的类似问题:https ://stackoverflow.com/a/68964548/8177355 )

\n

我想知道是否有人可以帮助我或提供如何模拟这种特定的 Riverpod 状态架构的示例。

\n
\xe2\x95\x90\xe2\x95\x90\xe2\x95\xa1 EXCEPTION CAUGHT BY WIDGETS LIBRARY \xe2\x95\x9e\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\nThe following ProviderException was thrown building LanguagePicker(dirty, dependencies:\n[UncontrolledProviderScope], state: _ConsumerState#9493f):\nAn exception was thrown while building Provider<Locale>#1de97.\n\nThrown exception:\nAn exception was thrown while building StateNotifierProvider<LocaleStateNotifier,\nLocaleState>#473ab.\n\nThrown exception:\ntype \'Null\' is not a subtype of type \'() => void\'\n\nStack trace:\n#0      MockStateNotifier.addListener (package:state_notifier/state_notifier.dart:270:18)\n#1      StateNotifierProvider.create (package:riverpod/src/state_notifier_provider/base.dart:60:37)\n#2      ProviderElementBase._buildState (package:riverpod/src/framework/provider_base.dart:481:26)\n#3      ProviderElementBase.mount (package:riverpod/src/framework/provider_base.dart:382:5)\n...[hundreds more lines]\n
Run Code Online (Sandbox Code Playgroud)\n

示例代码

\n

河波德的东西

\n
\xe2\x95\x90\xe2\x95\x90\xe2\x95\xa1 EXCEPTION CAUGHT BY WIDGETS LIBRARY \xe2\x95\x9e\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\xe2\x95\x90\nThe following ProviderException was thrown building LanguagePicker(dirty, dependencies:\n[UncontrolledProviderScope], state: _ConsumerState#9493f):\nAn exception was thrown while building Provider<Locale>#1de97.\n\nThrown exception:\nAn exception was thrown while building StateNotifierProvider<LocaleStateNotifier,\nLocaleState>#473ab.\n\nThrown exception:\ntype \'Null\' is not a subtype of type \'() => void\'\n\nStack trace:\n#0      MockStateNotifier.addListener (package:state_notifier/state_notifier.dart:270:18)\n#1      StateNotifierProvider.create (package:riverpod/src/state_notifier_provider/base.dart:60:37)\n#2      ProviderElementBase._buildState (package:riverpod/src/framework/provider_base.dart:481:26)\n#3      ProviderElementBase.mount (package:riverpod/src/framework/provider_base.dart:382:5)\n...[hundreds more lines]\n
Run Code Online (Sandbox Code Playgroud)\n

小部件尝试测试

\n
import \'dart:ui\';\n\nimport \'package:flutter_riverpod/flutter_riverpod.dart\';\nimport \'package:freezed_annotation/freezed_annotation.dart\';\nimport \'package:riverpodlocalization/models/locale/locale_providers.dart\';\nimport \'package:riverpodlocalization/models/persistent_state.dart\';\nimport \'package:riverpodlocalization/utils/json_local_sync.dart\';\n\nimport \'locale_json_converter.dart\';\n\npart \'locale_state.freezed.dart\';\npart \'locale_state.g.dart\';\n\n// Fallback Locale\nconst Locale fallbackLocale = Locale(\'en\', \'US\');\n\nfinal localeStateProvider = StateNotifierProvider<LocaleStateNotifier, LocaleState>((ref) => LocaleStateNotifier(ref));\n\n@freezed\nclass LocaleState with _$LocaleState, PersistentState<LocaleState> {\n  const factory LocaleState({\n    @LocaleJsonConverter() @Default(fallbackLocale) @JsonKey() Locale locale,\n  }) = _LocaleState;\n\n  // Allow custom getters / setters\n  const LocaleState._();\n\n  static const _localStorageKey = \'persistentLocale\';\n\n  /// Local Save\n  /// Saves the settings to persistent storage\n  @override\n  Future<bool> localSave() async {\n    Map<String, dynamic> value = toJson();\n    try {\n      return await JsonLocalSync.save(key: _localStorageKey, value: value);\n    } catch (e) {\n      print(e);\n      return false;\n    }\n  }\n\n  /// Local Delete\n  /// Deletes the settings from persistent storage\n  @override\n  Future<bool> localDelete() async {\n    try {\n      return await JsonLocalSync.delete(key: _localStorageKey);\n    } catch (e) {\n      print(e);\n      return false;\n    }\n  }\n\n  /// Create the settings from Persistent Storage\n  /// (Static Factory Method supports Async reading of storage)\n  @override\n  Future<LocaleState?> fromStorage() async {\n    try {\n      var _value = await JsonLocalSync.get(key: _localStorageKey);\n      if (_value == null) {\n        return null;\n      }\n      var _data = LocaleState.fromJson(_value);\n      return _data;\n    } catch (e) {\n      rethrow;\n    }\n  }\n\n  // For Riverpod integrated toJson / fromJson json_serializable code generator\n  factory LocaleState.fromJson(Map<String, dynamic> json) => _$LocaleStateFromJson(json);\n}\n\nclass LocaleStateNotifier extends StateNotifier<LocaleState> {\n  final StateNotifierProviderRef ref;\n  LocaleStateNotifier(this.ref) : super(const LocaleState());\n\n  /// Initialize Locale\n  /// Can be run at startup to establish the initial local from storage, or the platform\n  /// 1. Attempts to restore locale from storage\n  /// 2. IF no locale in storage, attempts to set local from the platform settings\n  Future<void> initLocale() async {\n    // Attempt to restore from storage\n    bool _fromStorageSuccess = await ref.read(localeStateProvider.notifier).restoreFromStorage();\n\n    // If storage restore did not work, set from platform\n    if (!_fromStorageSuccess) {\n      ref.read(localeStateProvider.notifier).setLocale(ref.read(platformLocaleProvider));\n    }\n  }\n\n  /// Set Locale\n  /// Attempts to set the locale if it\'s in our list of supported locales.\n  /// IF NOT: get the first locale that matches our language code and set that\n  /// ELSE: do nothing.\n  void setLocale(Locale locale) {\n    List<Locale> _supportedLocales = ref.read(supportedLocalesProvider);\n\n    // Set the locale if it\'s in our list of supported locales\n    if (_supportedLocales.contains(locale)) {\n      // Update state\n      state = state.copyWith(locale: locale);\n\n      // Save to persistence\n      state.localSave();\n      return;\n    }\n\n    // Get the closest language locale and set that instead\n    Locale? _closestLocale =\n        _supportedLocales.firstWhereOrNull((supportedLocale) => supportedLocale.languageCode == locale.languageCode);\n    if (_closestLocale != null) {\n      // Update state\n      state = state.copyWith(locale: _closestLocale);\n\n      // Save to persistence\n      state.localSave();\n      return;\n    }\n\n    // Otherwise, do nothing and we\'ll stick with the default locale\n    return;\n  }\n\n  /// Restore Locale from Storage\n  Future<bool> restoreFromStorage() async {\n    try {\n      print("Restoring LocaleState from storage.");\n      // Attempt to get the user from storage\n      LocaleState? _state = await state.fromStorage();\n\n      // If user is null, there is no user to restore\n      if (_state == null) {\n        return false;\n      }\n\n      print("State found in storage: " + _state.toJson().toString());\n\n      // Set state\n      state = _state;\n\n      return true;\n    } catch (e, s) {\n      print("Error" + e.toString());\n      print(s);\n      return false;\n    }\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

测试文件

\n
import \'package:flutter/material.dart\';\nimport \'package:flutter_riverpod/flutter_riverpod.dart\';\nimport \'package:riverpodlocalization/models/locale/locale_providers.dart\';\nimport \'package:riverpodlocalization/models/locale/locale_state.dart\';\nimport \'package:riverpodlocalization/models/locale/locale_translate_name.dart\';\n\nclass LanguagePicker extends ConsumerWidget {\n  const LanguagePicker({Key? key}) : super(key: key);\n\n  @override\n  Widget build(BuildContext context, WidgetRef ref) {\n    Locale _currentLocale = ref.watch(localeProvider);\n    List<Locale> _supportedLocales = ref.read(supportedLocalesProvider);\n\n    print("Current Locale: " + _currentLocale.toLanguageTag());\n\n    return DropdownButton<Locale>(\n        isDense: true,\n        value: (!_supportedLocales.contains(_currentLocale)) ? null : _currentLocale,\n        icon: const Icon(Icons.arrow_drop_down),\n        underline: Container(\n          height: 1,\n          color: Colors.black26,\n        ),\n        onChanged: (Locale? newLocale) {\n          if (newLocale == null) {\n            return;\n          }\n          print("Selected " + newLocale.toString());\n\n          // Set the locale (this will rebuild the app)\n          ref.read(localeStateProvider.notifier).setLocale(newLocale);\n\n          return;\n        },\n        // Create drop down items from our supported locales\n        items: _supportedLocales\n            .map<DropdownMenuItem<Locale>>(\n              (locale) => DropdownMenuItem<Locale>(\n                value: locale,\n                child: Padding(\n                  padding: const EdgeInsets.symmetric(horizontal: 8.0),\n                  child: Text(\n                    translateLocaleName(locale: locale),\n                  ),\n                ),\n              ),\n            )\n            .toList());\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Mat*_*out 4

示例存储库

我能够使用 StateNotifierProvider 成功模拟状态/提供者。我在这里创建了一个独立的存储库并进行了细分: https: //github.com/mdrideout/testing-state-notifier-provider

这无需 Mockito / Mocktail 即可工作。

如何

为了在使用 StateNotifier 和 StateNotifierProvider 时模拟您的状态,您的 StateNotifier 类必须包含状态模型的可选参数,以及状态应如何初始化的默认值。在您的测试中,您可以将具有预定义状态的模拟提供程序传递给您的测试小部件,并使用overrides来覆盖您的模拟提供程序。

细节

请参阅上面链接的存储库以获取完整代码

测试小部件

Widget isEvenTestWidget(StateNotifierProvider<CounterNotifier, Counter> mockProvider) {
    return ProviderScope(
      overrides: [
        counterProvider.overrideWithProvider(mockProvider),
      ],
      child: const MaterialApp(
        home: ScreenHome(),
      ),
    );
  }
Run Code Online (Sandbox Code Playgroud)

我们的主屏幕的这个测试小部件使用 的overrides属性ProviderScope()来覆盖小部件中使用的提供程序。

当 home.dartScreenHome()小部件调用时Counter counter = ref.watch(counterProvider);,它将使用我们的mockProvider而不是“真正的”提供者。

mockProvider参数isEvenTestWidget()与提供者的“类型”相同counterProvider()

考试

testWidgets('If count is even, IsEvenMessage is rendered.', (tester) async {
  // Mock a provider with an even count
  final mockCounterProvider =
      StateNotifierProvider<CounterNotifier, Counter>((ref) => CounterNotifier(counter: const Counter(count: 2)));

  await tester.pumpWidget(isEvenTestWidget(mockCounterProvider));

  expect(find.byType(IsEvenMessage), findsOneWidget);
});
Run Code Online (Sandbox Code Playgroud)

在测试中,我们使用测试小部件渲染所需的预定义值创建一个模拟提供程序ScreenHome()在此示例中,我们的提供程序使用state 进行初始化count: 2

我们正在测试isEvenMessage()小部件是否以偶数(2)呈现。另一个测试测试小部件未以奇数计数呈现。

状态通知器构造函数

class CounterNotifier extends StateNotifier<Counter> {
  CounterNotifier({Counter counter = const Counter(count: 0)}) : super(counter);

  void increment() {
    state = state.copyWith(count: state.count + 1);
  }
}
Run Code Online (Sandbox Code Playgroud)

为了能够创建具有预定义状态的mockProvider,StateNotifier( counter_state.dart)构造函数包含状态模型的可选参数是很重要的。默认参数是状态通常应该如何初始化。我们的测试可以选择提供指定的测试状态,并将其传递给super().