我的一些小部件具有根据状态显示/隐藏元素的条件UI。我正在尝试设置根据状态(例如,用户角色)查找或不查找小部件的测试。我下面的代码示例被剥离到一个小部件及其状态的基本信息,因为我似乎无法获得我的状态架构的最基本实现来处理模拟。
当我遵循其他的例子,如下面:
- https://bartvwezel.nl/flutter/flutter-riverpod-testing-example/
- https://stackoverflow.com/a/66122516/8177355
我无法访问覆盖数组中的.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());
});
}
2条答案
按热度按时间wwodge7n1#
示例存储库
我能够用StateNotifierProvider成功地模拟状态/提供者。我在这里创建了一个独立的存储库,并进行了分解:https://github.com/mdrideout/testing-state-notifier-provider
这在没有Mockito / Mocktail的情况下也可以使用。
如何操作
为了在使用StateNotifier和StateNotifierProvider时模拟您的状态,您的StateNotifier类必须包含状态模型的一个可选参数,以及状态初始化方式的默认值。在测试中,您可以将具有预定义状态的模拟提供程序传递给测试小部件,并使用
overrides
覆盖模拟提供程序。详细信息
测试小部件
这个主屏幕的测试小部件使用
ProviderScope()
的overrides
属性来覆盖小部件中使用的提供程序。当home.dart
ScreenHome()
小部件调用Counter counter = ref.watch(counterProvider);
时,它将使用我们的mockProvider
,而不是“真正的”提供者。isEvenTestWidget()
mockProvider参数是与counterProvider()
相同的提供程序“类型”。"考验"
在测试中,我们创建了一个mockProvider,其中包含测试
ScreenHome()
小部件呈现所需的预定义值。我们正在测试
isEvenMessage()
小部件是否以偶数计数(2)呈现。另一个测试则测试该小部件是否以奇数计数呈现。状态通告程序构造函数
为了能够创建具有预定义状态的mockProvider,StateNotifier(
counter_state.dart
)构造函数必须包含状态模型的可选参数。默认参数是状态通常应如何初始化的参数。我们的测试可以可选地为测试提供指定的状态,并将其传递给super()
。vlju58qv2#
基于@Matthew的答案,方法稍微灵活一些