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

ryhaxcpt  于 2022-11-08  发布在  其他
关注(0)|答案(2)|浏览(143)

我的一些小部件具有根据状态显示/隐藏元素的条件UI。我正在尝试设置根据状态(例如,用户角色)查找或不查找小部件的测试。我下面的代码示例被剥离到一个小部件及其状态的基本信息,因为我似乎无法获得我的状态架构的最基本实现来处理模拟。
当我遵循其他的例子,如下面:

我无法访问覆盖数组中的.state值。在尝试运行测试时,我也收到了以下错误。mocktail和mockito也是如此。我只能访问要覆盖的.notifier值(请参阅此处答案下的注解中的类似问题:(https://stackoverflow.com/a/68964548/8177355
我想知道是否有人可以帮助我或提供一个例子,一个人将如何嘲笑这个特殊的河荚国家建筑。

══╡ 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]

示例代码

"河荚号的东西"

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());
  });
}
wwodge7n

wwodge7n1#

示例存储库

我能够用StateNotifierProvider成功地模拟状态/提供者。我在这里创建了一个独立的存储库,并进行了分解:https://github.com/mdrideout/testing-state-notifier-provider
这在没有Mockito / Mocktail的情况下也可以使用。

如何操作

为了在使用StateNotifier和StateNotifierProvider时模拟您的状态,您的StateNotifier类必须包含状态模型的一个可选参数,以及状态初始化方式的默认值。在测试中,您可以将具有预定义状态的模拟提供程序传递给测试小部件,并使用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);
});

在测试中,我们创建了一个mockProvider,其中包含测试ScreenHome()小部件呈现所需的预定义值。
我们正在测试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);
  }
}

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

vlju58qv

vlju58qv2#

基于@Matthew的答案,方法稍微灵活一些

class CounterNotifier extends StateNotifier<Counter> {
  //No optional param needed
  CounterNotifier() : super(const Counter(count: 0));

  void increment() {
    state = state.copyWith(count: state.count + 1);
  }
}
class MockCounterNotifier extends CounterNotifier {

 set debugState(User value) {
   state = value;
 }
}

final mockCounterNotifier = MockCounterNotifier();
//inject into container
mockCounterNotifier.debugState = Counter(count: 2) 
container.pump()

相关问题