繁体   English   中英

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

[英]Riverpod Testing: How to mock state with StateNotifierProvider?

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

当我按照以下其他示例进行操作时:

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

我想知道是否有人可以帮助我或提供如何使用这个特定的 Riverpod state 架构进行模拟的示例。

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following ProviderException was thrown building LanguagePicker(dirty, dependencies:
[UncontrolledProviderScope], state: _ConsumerState#9493f):
An exception was thrown while building Provider<Locale>#1de97.

Thrown exception:
An exception was thrown while building StateNotifierProvider<LocaleStateNotifier,
LocaleState>#473ab.

Thrown exception:
type 'Null' is not a subtype of type '() => void'

Stack trace:
#0      MockStateNotifier.addListener (package:state_notifier/state_notifier.dart:270:18)
#1      StateNotifierProvider.create (package:riverpod/src/state_notifier_provider/base.dart:60:37)
#2      ProviderElementBase._buildState (package:riverpod/src/framework/provider_base.dart:481:26)
#3      ProviderElementBase.mount (package:riverpod/src/framework/provider_base.dart:382:5)
...[hundreds more lines]

示例代码

Riverpod的东西

import 'dart:ui';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpodlocalization/models/locale/locale_providers.dart';
import 'package:riverpodlocalization/models/persistent_state.dart';
import 'package:riverpodlocalization/utils/json_local_sync.dart';

import 'locale_json_converter.dart';

part 'locale_state.freezed.dart';
part 'locale_state.g.dart';

// Fallback Locale
const Locale fallbackLocale = Locale('en', 'US');

final localeStateProvider = StateNotifierProvider<LocaleStateNotifier, LocaleState>((ref) => LocaleStateNotifier(ref));

@freezed
class LocaleState with _$LocaleState, PersistentState<LocaleState> {
  const factory LocaleState({
    @LocaleJsonConverter() @Default(fallbackLocale) @JsonKey() Locale locale,
  }) = _LocaleState;

  // Allow custom getters / setters
  const LocaleState._();

  static const _localStorageKey = 'persistentLocale';

  /// Local Save
  /// Saves the settings to persistent storage
  @override
  Future<bool> localSave() async {
    Map<String, dynamic> value = toJson();
    try {
      return await JsonLocalSync.save(key: _localStorageKey, value: value);
    } catch (e) {
      print(e);
      return false;
    }
  }

  /// Local Delete
  /// Deletes the settings from persistent storage
  @override
  Future<bool> localDelete() async {
    try {
      return await JsonLocalSync.delete(key: _localStorageKey);
    } catch (e) {
      print(e);
      return false;
    }
  }

  /// Create the settings from Persistent Storage
  /// (Static Factory Method supports Async reading of storage)
  @override
  Future<LocaleState?> fromStorage() async {
    try {
      var _value = await JsonLocalSync.get(key: _localStorageKey);
      if (_value == null) {
        return null;
      }
      var _data = LocaleState.fromJson(_value);
      return _data;
    } catch (e) {
      rethrow;
    }
  }

  // For Riverpod integrated toJson / fromJson json_serializable code generator
  factory LocaleState.fromJson(Map<String, dynamic> json) => _$LocaleStateFromJson(json);
}

class LocaleStateNotifier extends StateNotifier<LocaleState> {
  final StateNotifierProviderRef ref;
  LocaleStateNotifier(this.ref) : super(const LocaleState());

  /// Initialize Locale
  /// Can be run at startup to establish the initial local from storage, or the platform
  /// 1. Attempts to restore locale from storage
  /// 2. IF no locale in storage, attempts to set local from the platform settings
  Future<void> initLocale() async {
    // Attempt to restore from storage
    bool _fromStorageSuccess = await ref.read(localeStateProvider.notifier).restoreFromStorage();

    // If storage restore did not work, set from platform
    if (!_fromStorageSuccess) {
      ref.read(localeStateProvider.notifier).setLocale(ref.read(platformLocaleProvider));
    }
  }

  /// Set Locale
  /// Attempts to set the locale if it's in our list of supported locales.
  /// IF NOT: get the first locale that matches our language code and set that
  /// ELSE: do nothing.
  void setLocale(Locale locale) {
    List<Locale> _supportedLocales = ref.read(supportedLocalesProvider);

    // Set the locale if it's in our list of supported locales
    if (_supportedLocales.contains(locale)) {
      // Update state
      state = state.copyWith(locale: locale);

      // Save to persistence
      state.localSave();
      return;
    }

    // Get the closest language locale and set that instead
    Locale? _closestLocale =
        _supportedLocales.firstWhereOrNull((supportedLocale) => supportedLocale.languageCode == locale.languageCode);
    if (_closestLocale != null) {
      // Update state
      state = state.copyWith(locale: _closestLocale);

      // Save to persistence
      state.localSave();
      return;
    }

    // Otherwise, do nothing and we'll stick with the default locale
    return;
  }

  /// Restore Locale from Storage
  Future<bool> restoreFromStorage() async {
    try {
      print("Restoring LocaleState from storage.");
      // Attempt to get the user from storage
      LocaleState? _state = await state.fromStorage();

      // If user is null, there is no user to restore
      if (_state == null) {
        return false;
      }

      print("State found in storage: " + _state.toJson().toString());

      // Set state
      state = _state;

      return true;
    } catch (e, s) {
      print("Error" + e.toString());
      print(s);
      return false;
    }
  }
}

小部件尝试测试

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpodlocalization/models/locale/locale_providers.dart';
import 'package:riverpodlocalization/models/locale/locale_state.dart';
import 'package:riverpodlocalization/models/locale/locale_translate_name.dart';

class LanguagePicker extends ConsumerWidget {
  const LanguagePicker({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    Locale _currentLocale = ref.watch(localeProvider);
    List<Locale> _supportedLocales = ref.read(supportedLocalesProvider);

    print("Current Locale: " + _currentLocale.toLanguageTag());

    return DropdownButton<Locale>(
        isDense: true,
        value: (!_supportedLocales.contains(_currentLocale)) ? null : _currentLocale,
        icon: const Icon(Icons.arrow_drop_down),
        underline: Container(
          height: 1,
          color: Colors.black26,
        ),
        onChanged: (Locale? newLocale) {
          if (newLocale == null) {
            return;
          }
          print("Selected " + newLocale.toString());

          // Set the locale (this will rebuild the app)
          ref.read(localeStateProvider.notifier).setLocale(newLocale);

          return;
        },
        // Create drop down items from our supported locales
        items: _supportedLocales
            .map<DropdownMenuItem<Locale>>(
              (locale) => DropdownMenuItem<Locale>(
                value: locale,
                child: Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 8.0),
                  child: Text(
                    translateLocaleName(locale: locale),
                  ),
                ),
              ),
            )
            .toList());
  }
}

测试文件

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:riverpodlocalization/models/locale/locale_state.dart';
import 'package:riverpodlocalization/widgets/language_picker.dart';

class MockStateNotifier extends Mock implements LocaleStateNotifier {}

void main() {
  final mockStateNotifier = MockStateNotifier();

  Widget testingWidget() {
    return ProviderScope(
      overrides: [localeStateProvider.overrideWithValue(mockStateNotifier)],
      child: const MaterialApp(
        home: LanguagePicker(),
      ),
    );
  }

  testWidgets('Test that the pumpedWidget is loaded with our above mocked state', (WidgetTester tester) async {
    await tester.pumpWidget(testingWidget());
  });
}

示例存储库

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

这在没有 Mockito / Mocktail 的情况下有效。

如何

In order to mock your state when you are using StateNotifier and StateNotifierProvider, your StateNotifier class must contain an optional parameter of your state model, with a default value for how your state should initialize. 然后,在您的测试中,您可以将带有预定义 state 的模拟提供程序传递给您的测试小部件,并使用overrides来覆盖您的模拟提供程序。

细节

有关完整代码,请参阅上面链接的 repo

测试小部件

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

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

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

isEvenTestWidget() mockProvider 参数与提供者的“类型”相同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);
});

在测试中,我们使用测试ScreenHome()小部件渲染所需的预定义值创建了一个 mockProvider。 在这个例子中,我们的提供者被初始化为state count: 2

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

StateNotifier 构造函数

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

  void increment() {
    state = state.copyWith(count: state.count + 1);
  }
}

In order to be able to create a mockProvider with a predefined state, it is important that the StateNotifier ( counter_state.dart ) constructor includes an optional parameter of the state model. 默认参数是 state 应该如何正常初始化。 我们的测试可以选择提供指定的 state 用于传递给super()的测试。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM