Я работаю над личным проектом флаттера, используя Riverpod без хуков и генерации кода. Я только что реализовал локализацию приложения. Реализация состоит из одной кнопки, которая при нажатии меняет текущий язык на другой язык из-за использования только двух языков в приложении. Я использовал AutoDisposeAsyncNotifier, чтобы изменить состояние локализации приложения и сохранить локально через общие настройки текущего языкового стандарта. Я хотел бы провести модульные тесты для логики, но мне трудно понять из документации RiverPod, как правильно тестировать AsyncNotifier/Provider, который я использую.
Вот содержимое реализованного мной уведомления о локализации:
class LocalizationNotifier extends AutoDisposeAsyncNotifier<Locale> {
Locale currentLocale = const Locale('en');
@override
FutureOr<Locale> build() {
return SharedPreferencesService().getLocale();
}
void changeLocale(Locale newLocale) {
switch(currentLocale.languageCode) {
case 'en':
newLocale = const Locale('bg');
break;
case 'bg':
newLocale = const Locale('en');
break;
default:
newLocale = const Locale('en');
break;
}
currentLocale = newLocale;
SharedPreferencesService().setLocale(newLocale);
ref.invalidateSelf();
}
}
final localizationProvider = AsyncNotifierProvider.autoDispose<LocalizationNotifier ,Locale>(
LocalizationNotifier.new,
);
Вот код методов службы общих настроек, которые я использую в уведомителе, который хочу протестировать:
Future<void> setLocale(Locale locale) async {
switch(locale.languageCode){
case 'en': await _prefs.setString(SharedPreferencesKeys.locale, 'en');
break;
case 'bg': await _prefs.setString(SharedPreferencesKeys.locale, 'bg');
break;
default: await _prefs.setString(SharedPreferencesKeys.locale, 'en');
break;
}
}
Locale getLocale() {
String languageCode = _prefs.getString(SharedPreferencesKeys.locale) ?? 'en';
return Locale(languageCode);
}
Я ломал голову над документацией, даже пробовал использовать
Судя по тому, что я прочитал в документации, я не хочу издеваться над уведомителем, а вместо этого действительно издеваюсь над экземпляром общих настроек. Но как мне протестировать отдельные методы уведомления, используя только поставщика?





После некоторых исследований архитектуры приложений Flutter, шаблонов проектирования и тестирования в целом я пришел к выводу, что основная проблема заключается в структуре моего приложения Flutter. Согласно документации Riverpod, тестирование Notifiers не рекомендуется, вместо этого службы должны быть реализованы таким образом и абстракционно, чтобы их методы и функциональность должны были быть протестированы. Теперь, чтобы исправить свой код, я провел рефакторинг для реализации внедрения зависимостей, чтобы сделать методы более тестируемыми и обойти тестирование Notifier.
Имейте в виду, что после того, как я задал вопрос, изначально я реорганизовал свой код, чтобы использовать генерацию кода, поэтому ответ будет использовать генерацию кода Riverpod.
Код класса репозитория:
part 'shared_preferences_repository.g.dart';
class SharedPreferencesRepository {
final SharedPreferences _sharedPrefs;
SharedPreferencesRepository(this._sharedPrefs);
Future<void> setLocale(Locale locale) async {
await _sharedPrefs.setString(SharedPreferencesKeys.locale, locale.languageCode);
}
Locale getLocale() {
String languageCode = _sharedPrefs.getString(SharedPreferencesKeys.locale) ?? 'en';
return Locale(languageCode);
}
}
class SharedPreferencesKeys {
static const String locale = 'locale';
}
@riverpod
SharedPreferencesRepository sharedPreferences(SharedPreferencesRef ref) {
throw UnimplementedError();
}
Я реализую SharePreferencesProvider, потому что я сделаю это, переопределив его в основном методе, где он инициализируется:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final sharedPreferences = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(
SharedPreferencesRepository(sharedPreferences),
),
],
child: const MyApp(),
),
);
}
Контроллер был реорганизован и теперь выглядит следующим образом:
part 'localization_controller.g.dart';
@riverpod
class LocalizationController extends _$LocalizationController {
late SharedPreferencesRepository _sharedPrefs;
@override
Locale build() {
_sharedPrefs = ref.read(sharedPreferencesProvider);
return _sharedPrefs.getLocale();
}
void changeLocale() async {
final locale = _determineNewLocale(state);
await _sharedPrefs.setLocale(locale);
state = _sharedPrefs.getLocale();
}
Locale _determineNewLocale(Locale locale) {
switch (locale.languageCode) {
case 'en':
return const Locale('bg');
case 'bg':
return const Locale('en');
default:
return locale;
}
}
}
И наконец тесты:
ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
addTearDown(container.dispose);
return container;
}
void main() {
// Declaring variables
late ProviderContainer container;
final Map<String, Object> initialLocale = <String, Object>{SharedPreferencesKeys.locale: 'en'};
//Setup
setUp(() async {
TestWidgetsFlutterBinding.ensureInitialized();
SharedPreferences.setMockInitialValues(initialLocale);
final sharedPrefs = await SharedPreferences.getInstance();
container = createContainer(
overrides: [sharedPreferencesProvider.overrideWithValue(SharedPreferencesRepository(sharedPrefs))],
);
});
//Tests
group("Localization Repository Tests", (){
test("Testing getLocale on initial value.", () {
expect(container.read(sharedPreferencesProvider).getLocale(), const Locale('en'));
});
test("Testing Changing the Locale.", () async {
await container.read(sharedPreferencesProvider).setLocale(const Locale('bg'));
expect(container.read(sharedPreferencesProvider).getLocale(), const Locale('bg'));
});
test("Testing Changing the Locale multiple times.", () async {
await container.read(sharedPreferencesProvider).setLocale(const Locale('bg'));
expect(container.read(sharedPreferencesProvider).getLocale(), const Locale('bg'));
await container.read(sharedPreferencesProvider).setLocale(const Locale('en'));
expect(container.read(sharedPreferencesProvider).getLocale(), const Locale('en'));
});
});
}
Как видно, я тестирую методы класса репозитория через их провайдера, а не через уведомитель.
В этом случае вам нужно только имитировать экземпляр общих настроек. Вы хотите протестировать логику методов уведомителя, поэтому не хотите создавать альтернативную реализацию для уведомителя, верно?