Обучение TDD с чистой архитектурой для Flutter (слой домена) Часть II

Часть I здесь
Добро пожаловать во вторую часть этой серии. Давайте продолжим с того места, на котором остановились. Здесь мы начнем кодировать и завершим TDD для доменного слоя.

Чистая архитектура TDD

Теперь мы знаем немного о TDD и чистой архитектуре, давайте объединим эти два подхода, чтобы получить лучшее из обоих миров. Прежде чем приступить к этому, давайте проясним несколько моментов:

  • Мы будем писать модульный тест.
  • Мы будем использовать Mocktail для моделирования зависимостей.
 class MockAwsDataSource extends Mock implements AwsDataSource {} 
    // To mock class AwsDataSourece using mocktail
Вход в полноэкранный режим Выход из полноэкранного режима
  • При тестировании любого класса, подражайте всем классам/зависимостям внутри него, кроме класса, который мы тестируем, и контролируйте все функции внутри этих классов.
  • У вас должна быть похожая структура папок внутри тестовой папки, т.е. имитируйте структуру папки lib внутри тестовой папки.

У вас должны быть базовые знания о dart и абстракции.

Слой домена

Это один из самых важных слоев. Он должен оставаться неизменным, даже если меняются все остальные слои. Мы всегда должны начинать с доменного слоя.

Доменный слой состоит из сущностей, хранилищ и примеров использования.

Сущности

Это легкий объект домена и основная структура данных, которая нам абсолютно необходима.

Например, сущность фильма lib/features/movie/domain/entities/movie_entity.dart

 import 'package:equatable/equatable.dart';

    class MovieEntity extends Equatable {
      final String movieId;
      final String title;
      final String thumbnail;
      final String movieUrl;
      final bool unlocked;
      const MovieEntity({
        required this.movieId,
        required this.title,
        required this.thumbnail,
        required this.movieUrl,
        required this.unlocked,
      });

    @override
      List<Object> get props {
        return [
          movieId,
          title,
          thumbnail,
          movieUrl,
          unlocked,
        ];
      }
    }
Вход в полноэкранный режим Выйти из полноэкранного режима

Equatable переопределяет == и hashCode для вас, так что вам не придется тратить свое время на написание большого количества кода.

Репозитории

Он находится как на уровне домена, так и на уровне данных. Его определение находится на доменном уровне, а реализация — на уровне данных, что обеспечивает полную независимость от доменного уровня.

Например, репозиторий фильма lib/features/movie/domain/repositories/movie_repository.dart

  import 'package:dartz/dartz.dart';
    import '../../../../core/error/failure.dart';
    import '../entities/movie_entity.dart';
    abstract class MovieRepository {
      Future<Either<Failure, List<MovieEntity>>> getMovieList();
    }
Вход в полноэкранный режим Выйти из полноэкранного режима

В нем будет метод для получения списка фильмов, т.е. списка сущностей фильмов. Future>> обеспечит обработку ошибок. Давайте двигаться дальше. Минуточку, что такое ключевое слово either? Оно позволяет функции вернуть либо отказ, либо список сущностей фильма, но не то и другое. Оно было получено из пакета Dartz, который предоставляет возможности функционального программирования в dart. Класс Failure был определен в файле core/error для определения отказа кода.

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

Например, класс Failure определен в lib/core/error/failure.dart.

import 'package:equatable/equatable.dart';
    abstract class Failure extends Equatable {
     @override
      List<Object?> get props => [];
    }
    //General Failures
    class ServerFailure extends Failure {}
    class CacheFailure extends Failure {}

Вход в полноэкранный режим Выйти из полноэкранного режима

Примеры использования

Примеры использования — это место, где выполняется бизнес-логика. Он вызывает хранилище для получения данных.

Например, Usecase для получения списка фильмов lib/features/movie/domain/usecases/get_movie_list_usecase.dart

    import 'package:dartz/dartz.dart';
    import '../../../../core/error/failure.dart';
    import '../../../../core/usecases/usecase.dart';
    import '../entities/movie_entity.dart';
    import '../repositories/movie_repository.dart';

    class GetMovieListUsecase implements UseCase<List<MovieEntity>, NoParams> {
      final MovieRepository movieRepository;
      GetMovieListUsecase({
        required this.movieRepository,
      });

    @override
      Future<Either<Failure, List<MovieEntity>>> call(NoParams params) async {
        return await movieRepository.getMovieList();
      }
    }

Вход в полноэкранный режим Выйти из полноэкранного режима

GetVideoLiseUsecase реализует use-case внутри lib/core/use-case . Там мы определяем правила для сценариев использования.

Например, базовый сценарий использования определен в lib/core/usecases/usecase.dart

 import 'package:dartz/dartz.dart';
    import 'package:equatable/equatable.dart';
    import '../error/failure.dart';
    abstract class UseCase<Type, Params> {
      Future<Either<Failure, Type>> call(Params params);
    }

    class NoParams extends Equatable {
      @override
      List<Object?> get props => [];
    }

Вход в полноэкранный режим Выйти из полноэкранного режима

Минуточку, это должна была быть чистая архитектура TDD. Разве мы не должны сначала написать тест?

Да, прежде чем писать сценарий использования, мы должны сначала написать тест. Представьте, что приведенный выше код еще не написан, и давайте напишем тест.

Файл теста должен заканчиваться ‘_test.dart’, и вы можете запустить тест, набрав flutter test в терминале, или запустить его вручную, нажав кнопку run. При именовании теста дайте ему то же имя, что и файлу, который вы пытаетесь протестировать, и добавьте к нему ‘ _test ‘.

Например, тест для сценария использования test/features/movie/domain/usecases/get_movie_list_usecase_test.dart

 import 'package:dartz/dartz.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mocktail/mocktail.dart';

class MockMovieRepository extends Mock implements MovieRepository {}
  void main() {
      late GetMovieListUsecase getMovieListUsecase;
      late MockMovieRepository mockMovieRepository;
      final tMovieList = [
        const MovieEntity(
          movieId: 'movieId',title: 'title',thumbnail: 'thumbnail',
      movieUrl: 'movieUrl',unlocked: true,
        ),
        const MovieEntity(
          movieId: 'movieIds',title: 'titles',thumbnail: 'thumbnails',
      movieUrl: 'movieUrls',unlocked: false,
        )   
      ];
      setUp(() {
        mockMovieRepository = MockMovieRepository();
        getMovieListUsecase =
            GetMovieListUsecase(movieRepository: mockMovieRepository);
      });
      test(
        'should get list of movie',
        () async {
          // arrange
          when(() => mockMovieRepository.getMovieList())
              .thenAnswer((_) async => Right(tMovieList));

    // act
          final result = await getMovieListUsecase(NoParams());

    // assert
          verify(() => mockMovieRepository.getMovieList());

    expect(result, Right(tMovieList));
          verifyNoMoreInteractions(mockMovieRepository);
        },
      );
    }

Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь давайте объясним код

Мы тестируем GetMovieListUsecase в get_movie_list_usecase.dart, который реализует базовый вариант использования в lib/core/usecases/usecase.dart . Пример использования GetMovieListUsecase вызывает хранилище данных, поэтому хранилище должно быть сымитировано, чтобы протестировать пример использования. После подражания хранилищу мы напишем тест.

Теперь, как и в исходном файле dart, есть главный файл, который будет выполняться. Главная функция содержит тест.

Сначала мы определяем все необходимые нам зависимости.

Затем мы инстанцируем их в setUp. Если посмотреть на тест, то он состоит из трех частей.

В arrange мы контролируем результат работы функций.

В act мы запускаем функции.

В assert мы проверяем успешность теста. Функция verify проверяет, что функция была выполнена. expect сравнивает результат, а verifyNoMoreInteractions гарантирует, что хранилище больше не используется или с ним не взаимодействуют.

Если мы присмотримся, то увидим, что тест похож на документацию, объясняющую нам, что должен делать код.

Вы можете задаться вопросом, если usecase еще не создан, как нам отобразить или визуализировать тест. Здесь на помощь приходит абстракция. Даже если getMovieListUsecase еще не создан, usecase был определен с помощью абстрактного класса. Мы можем использовать этот абстрактный класс для визуализации того, каким будет класс usecase, и использовать его для написания теста (в большинстве случаев). Поэтому интерфейс/абстрактный класс очень важны в TDD. После написания теста мы пишем usecase до успешного завершения теста и рефакторим его по мере необходимости.

В доменном слое нужно тестировать только use-case, так как хранилище и сущность тестировать не нужно, поскольку они очень просты.

Наконец, у нас есть следующий файл и папка

Доменный уровень в lib
Доменный слой в test

В моем следующем посте мы внедрим TDD для уровня данных.

Оцените статью
devanswers.ru
Добавить комментарий