Как правильно проводить тестирование во Flutter Riverpod?

Я работаю над личным проектом флаттера, используя 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);
  }

Я ломал голову над документацией, даже пробовал использовать

В этом случае вам нужно только имитировать экземпляр общих настроек. Вы хотите протестировать логику методов уведомителя, поэтому не хотите создавать альтернативную реализацию для уведомителя, верно?

Dhafin Rayhan 18.06.2024 05:55

Судя по тому, что я прочитал в документации, я не хочу издеваться над уведомителем, а вместо этого действительно издеваюсь над экземпляром общих настроек. Но как мне протестировать отдельные методы уведомления, используя только поставщика?

Atanas Nankinski 18.06.2024 09:13
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
101
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

После некоторых исследований архитектуры приложений 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'));
    });
  });
}

Как видно, я тестирую методы класса репозитория через их провайдера, а не через уведомитель.

Другие вопросы по теме