【发布时间】:2021-12-28 19:46:23
【问题描述】:
我的一些小部件具有根据状态显示/隐藏元素的条件 UI。我正在尝试设置根据状态(例如,用户角色)查找或不查找小部件的测试。我下面的代码示例被精简为一个小部件及其状态的基础知识,因为我似乎无法让我的状态架构的最基本实现与模拟一起使用。
当我遵循以下其他示例时:
- https://bartvwezel.nl/flutter/flutter-riverpod-testing-example/
- https://stackoverflow.com/a/66122516/8177355
我无法访问覆盖数组中的.state 值。尝试运行测试时,我也收到以下错误。这与mocktail 和mockito 相同。我只能访问要覆盖的 .notifier 值(请参阅此处答案下的 cmets 中的类似问题:https://stackoverflow.com/a/68964548/8177355)
我想知道是否有人可以帮助我或提供如何模拟这种特定的 Riverpod 状态架构的示例。
══╡ 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());
});
}
【问题讨论】:
标签: flutter dart testing mockito