Flutter Arquitectura Limpia [6] – Implementación del Repositorio

[wp_ad_camp_1]

Flutter Arquitectura Limpia [6] – Implementación del Repositorio

 

Después de la parte anterior, ahora tenemos todos los contratos de las dependencias del Repositorio en su lugar. Esas dependencias son el origen de datos local y remoto y también la clase NetworkInfo, para averiguar si el usuario está en línea. Haciendo mocking de estas dependencias nos permitirá implementar la clase Repository utilizando un desarrollo basado en pruebas.

Debo aclarar que el contenido original es de Resocoder,  lo que he hecho  es una traducción al español del contenido. Al final de este artículo está el video en inglés para que vayan ilustrándose mejor y ya tengan una claridad de lo expuesto

Curso TDD Arquitectura Limpia

001 draw
Esta publicación es solo una parte de una serie de tutoriales. ¡Vea todas las otras partes y aprenda a diseñar sus aplicaciones Flutter!
[wp_ad_camp_2]
Índice()

    Implementando el repositorio

    Si necesita una actualización rápida sobre dónde terminamos en la parte anterior, actualmente tenemos un archivo de prueba con dependencias simuladas ...

    number_trivia_repository_impl_test.dart

    class MockRemoteDataSource extends Mock
        implements NumberTriviaRemoteDataSource {}
    
    class MockLocalDataSource extends Mock implements NumberTriviaLocalDataSource {}
    
    class MockNetworkInfo extends Mock implements NetworkInfo {}
    
    void main() {
      NumberTriviaRepositoryImpl repository;
      MockRemoteDataSource mockRemoteDataSource;
      MockLocalDataSource mockLocalDataSource;
      MockNetworkInfo mockNetworkInfo;
    
      setUp(() {
        mockRemoteDataSource = MockRemoteDataSource();
        mockLocalDataSource = MockLocalDataSource();
        mockNetworkInfo = MockNetworkInfo();
        repository = NumberTriviaRepositoryImpl(
          remoteDataSource: mockRemoteDataSource,
          localDataSource: mockLocalDataSource,
          networkInfo: mockNetworkInfo,
        );
      });
    }

    ... y también un repositorio simple y sin implementación.

    number_trivia_repository_impl.dart

    class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
      final NumberTriviaRemoteDataSource remoteDataSource;
      final NumberTriviaLocalDataSource localDataSource;
      final NetworkInfo networkInfo;
    
      NumberTriviaRepositoryImpl({
        @required this.remoteDataSource,
        @required this.localDataSource,
        @required this.networkInfo,
      });
    
      @override
      Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
        // TODO: implement getConcreteNumberTrivia
        return null;
      }
    
      @override
      Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() {
        // TODO: implement getRandomNumberTrivia
        return null;
      }
    }

    Comencemos con la implementación del método getConcreteNumberTrivia primero.

    getConcreteNumberTrivia

    001 finger

    El trabajo del Repositorio es obtener datos nuevos de la API cuando hay una conexión a Internet (y luego almacenarlos en caché localmente), u obtener los datos almacenados en caché cuando el usuario está desconectado.

    Por lo tanto, conocer el estado de la red del dispositivo es lo primero que debe ocurrir dentro de este método. Escribamos una prueba siguiendo el flujo Organizar - Actuar - Afirmar. Estamos aplicando mocking en NetworkInfo y verificando si se ha llamado a su propiedad isConnected.

     

    [wp_ad_camp_3]

    number_trivia_repository_impl_test.dart

    group('getConcreteNumberTrivia', () {
      // DATA FOR THE MOCKS AND ASSERTIONS
      // We'll use these three variables throughout all the tests
      final tNumber = 1;
      final tNumberTriviaModel =
          NumberTriviaModel(number: tNumber, text: 'test trivia');
      final NumberTrivia tNumberTrivia = tNumberTriviaModel;
    
      test('should check if the device is online', () {
        //arrange
        when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
        // act
        repository.getConcreteNumberTrivia(tNumber);
        // assert
        verify(mockNetworkInfo.isConnected);
      });
    });

    No creo que necesite escribirlo más, pero esta prueba fallará. Tenemos que implementar la funcionalidad necesaria. Con las otras pruebas en esta parte, solo le mostraré el código de producción inmediatamente después del código de prueba.

    Te puede interesar:  Flutter y la creación de aplicaciones móvil y web

    number_trivia_repository_impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
      networkInfo.isConnected;
      return null;
    }

    "Pero ... ¡el código anterior no hace nada útil con el valor de isConnected!"
    ¡Eso es correcto! Nunca debe escribir más código de producción del que sea suficiente para pasar la prueba. Haremos que este método haga algo útil después de más pruebas.

    Como ya sabe, la funcionalidad del método diferirá en función de si el usuario está conectado o no. Por lo tanto, "ramificaremos" las pruebas en dos categorías: en línea y fuera de línea. Comencemos con la rama en línea.

    Comportamiento Online 

    test.dart

    group('device is online', () {
      // This setUp applies only to the 'device is online' group
      setUp(() {
        when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
      });
    
      test(
        'should return remote data when the call to remote data source is successful',
        () async {
          // arrange
          when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
              .thenAnswer((_) async => tNumberTriviaModel);
          // act
          final result = await repository.getConcreteNumberTrivia(tNumber);
          // assert
          verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
          expect(result, equals(Right(tNumberTrivia)));
        },
      );
    });

    impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
      int number,
    ) async {
      networkInfo.isConnected;
      return Right(await remoteDataSource.getConcreteNumberTrivia(number));
    }

    El lado derecho del Either es el "lado del éxito" que devuelve una entidad NumberTrivia. La implementación todavía no parece mucho, pero estamos llegando allí.

    Siempre que se obtengan con éxito las trivias de la API, debemos almacenarlas localmente. Eso es lo que vamos a implementar a continuación (aún dentro del grupo en línea): siempre puede obtener el proyecto completo de GitHub, si está perdido, para ver el código en un solo lugar después de seguir el tutorial.

    [wp_ad_camp_4]

    test.dart

    test(
      'should cache the data locally when the call to remote data source is successful',
      () async {
        // arrange
        when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
            .thenAnswer((_) async => tNumberTriviaModel);
        // act
        await repository.getConcreteNumberTrivia(tNumber);
        // assert
        verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
        verify(mockLocalDataSource.cacheNumberTrivia(tNumberTrivia));
      },
    );

    impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
      int number,
    ) async {
      networkInfo.isConnected;
      final remoteTrivia = await remoteDataSource.getConcreteNumberTrivia(number);
      localDataSource.cacheNumberTrivia(remoteTrivia);
      return Right(remoteTrivia);
    }

    Finalmente, cuando estamos en línea y la fuente de datos remota arroja una excepción ServerException, debemos convertirla en una falla de servidor y devolverla del método. En tal caso, nada debe almacenarse en caché localmente (por lo tanto, verificar ZeroInteractions).

     

    test.dart

    test(
      'should return server failure when the call to remote data source is unsuccessful',
      () async {
        // arrange
        when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
            .thenThrow(ServerException());
        // act
        final result = await repository.getConcreteNumberTrivia(tNumber);
        // assert
        verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
        verifyZeroInteractions(mockLocalDataSource);
        expect(result, equals(Left(ServerFailure())));
      },
    );

    impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
      int number,
    ) async {
      networkInfo.isConnected;
      try {
        final remoteTrivia =
            await remoteDataSource.getConcreteNumberTrivia(number);
        localDataSource.cacheNumberTrivia(remoteTrivia);
        return Right(remoteTrivia);
      } on ServerException {
        return Left(ServerFailure());
      }
    }

    Comportamiento Offline 

    Las pruebas anteriores son suficientes para cuando el dispositivo está en línea, ahora llega el momento de implementar el comportamiento fuera de línea. Creemos nuevamente un nuevo grupo de prueba junto con la primera prueba. El Repositorio debería devolver las últimas curiosidades almacenadas en caché local cuando no esté en línea.

    Te puede interesar:  Flutter Arquitectura Limpia [12] – Implementación de Bloc 2/2

    test.dart

    group('device is offline', () {
      setUp(() {
        when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
      });
    
      test(
        'should return last locally cached data when the cached data is present',
        () async {
          // arrange
          when(mockLocalDataSource.getLastNumberTrivia())
              .thenAnswer((_) async => tNumberTriviaModel);
          // act
          final result = await repository.getConcreteNumberTrivia(tNumber);
          // assert
          verifyZeroInteractions(mockRemoteDataSource);
          verify(mockLocalDataSource.getLastNumberTrivia());
          expect(result, equals(Right(tNumberTrivia)));
        },
      );
    });

    impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
      int number,
    ) async {
      // Finally doing something with the value of isConnected 😉
      if (await networkInfo.isConnected) {
        try {
          final remoteTrivia =
              await remoteDataSource.getConcreteNumberTrivia(number);
          localDataSource.cacheNumberTrivia(remoteTrivia);
          return Right(remoteTrivia);
        } on ServerException {
          return Left(ServerFailure());
        }
      } else {
        final localTrivia = await localDataSource.getLastNumberTrivia();
        return Right(localTrivia);
      }
    }

    También tenemos que manejar el caso cuando el origen de datos local arroja una CacheException devolviendo un CacheFailure a través del lado "error" izquierdo de Either. Como se escribe en la documentación del método getLastNumberTrivia, CacheException sucederá siempre que no haya nada presente en el caché.

    test.dart

    test(
      'should return CacheFailure when there is no cached data present',
      () async {
        // arrange
        when(mockLocalDataSource.getLastNumberTrivia())
            .thenThrow(CacheException());
        // act
        final result = await repository.getConcreteNumberTrivia(tNumber);
        // assert
        verifyZeroInteractions(mockRemoteDataSource);
        verify(mockLocalDataSource.getLastNumberTrivia());
        expect(result, equals(Left(CacheFailure())));
      },
    );

    impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
      int number,
    ) async {
      if (await networkInfo.isConnected) {
        try {
          final remoteTrivia =
              await remoteDataSource.getConcreteNumberTrivia(number);
          localDataSource.cacheNumberTrivia(remoteTrivia);
          return Right(remoteTrivia);
        } on ServerException {
          return Left(ServerFailure());
        }
      } else {
        try {
          final localTrivia = await localDataSource.getLastNumberTrivia();
          return Right(localTrivia);
        } on CacheException {
          return Left(CacheFailure());
        }
      }
    }

    getRandomNumberTrivia

    002 dice

     

    La forma en que construiremos getRandomNumberTrivia será casi idéntica a getConcreteNumberTrivia. Incluso podemos refactorizar algunas partes de las pruebas en métodos con nombre, a saber, los grupos en línea y fuera de línea. Refactorizado usando los dos nuevos métodos, el código de prueba actual se verá así (truncado por brevedad, obtenga el código completo en GitHub).

     

    [wp_ad_camp_5]

    test.dart

    void main() {
      ...
    
      void runTestsOnline(Function body) {
        group('device is online', () {
          setUp(() {
            when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
          });
    
          body();
        });
      }
    
      void runTestsOffline(Function body) {
        group('device is offline', () {
          setUp(() {
            when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
          });
    
          body();
        });
      }
    
      group('getConcreteNumberTrivia', () {
        ...
    
        runTestsOnline(() {
          test(
            ...
        });
    
        runTestsOffline(() {
          test(
            ...
        });
      });
    }

    Las leyes semioficiales de TDD dicen que siempre debe escribir e implementar pruebas una por una. Sin embargo, me gusta ser práctico, especialmente cuando se trata de codificar en tutoriales como este.

    Dado que el método getRandomNumberTrivia diferirá solo en una sola llamada a la Fuente de datos remota, vamos a copiar todas las pruebas que tenemos actualmente para el método concreto y modificarlas ligeramente para que funcionen con el método aleatorio.

    test.dart

    group('getRandomNumberTrivia', () {
      final tNumberTriviaModel =
          NumberTriviaModel(number: 123, text: 'test trivia');
      final NumberTrivia tNumberTrivia = tNumberTriviaModel;
    
      test('should check if the device is online', () {
        //arrange
        when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
        // act
        repository.getRandomNumberTrivia();
        // assert
        verify(mockNetworkInfo.isConnected);
      });
    
      runTestsOnline(() {
        test(
          'should return remote data when the call to remote data source is successful',
          () async {
            // arrange
            when(mockRemoteDataSource.getRandomNumberTrivia())
                .thenAnswer((_) async => tNumberTriviaModel);
            // act
            final result = await repository.getRandomNumberTrivia();
            // assert
            verify(mockRemoteDataSource.getRandomNumberTrivia());
            expect(result, equals(Right(tNumberTrivia)));
          },
        );
    
        test(
          'should cache the data locally when the call to remote data source is successful',
          () async {
            // arrange
            when(mockRemoteDataSource.getRandomNumberTrivia())
                .thenAnswer((_) async => tNumberTriviaModel);
            // act
            await repository.getRandomNumberTrivia();
            // assert
            verify(mockRemoteDataSource.getRandomNumberTrivia());
            verify(mockLocalDataSource.cacheNumberTrivia(tNumberTrivia));
          },
        );
    
        test(
          'should return server failure when the call to remote data source is unsuccessful',
          () async {
            // arrange
            when(mockRemoteDataSource.getRandomNumberTrivia())
                .thenThrow(ServerException());
            // act
            final result = await repository.getRandomNumberTrivia();
            // assert
            verify(mockRemoteDataSource.getRandomNumberTrivia());
            verifyZeroInteractions(mockLocalDataSource);
            expect(result, equals(Left(ServerFailure())));
          },
        );
      });
    
      runTestsOffline(() {
        test(
          'should return last locally cached data when the cached data is present',
          () async {
            // arrange
            when(mockLocalDataSource.getLastNumberTrivia())
                .thenAnswer((_) async => tNumberTriviaModel);
            // act
            final result = await repository.getRandomNumberTrivia();
            // assert
            verifyZeroInteractions(mockRemoteDataSource);
            verify(mockLocalDataSource.getLastNumberTrivia());
            expect(result, equals(Right(tNumberTrivia)));
          },
        );
    
        test(
          'should return CacheFailure when there is no cached data present',
          () async {
            // arrange
            when(mockLocalDataSource.getLastNumberTrivia())
                .thenThrow(CacheException());
            // act
            final result = await repository.getRandomNumberTrivia();
            // assert
            verifyZeroInteractions(mockRemoteDataSource);
            verify(mockLocalDataSource.getLastNumberTrivia());
            expect(result, equals(Left(CacheFailure())));
          },
        );
      });
    });

    En el espíritu de TDD, no haremos ninguna refactorización prematura. Primero implementemos el código ingenuamente, aunque ya sabemos que estará plagado de duplicaciones.

    Te puede interesar:  Flutter Arquitectura Limpia [8] – Fuente de Datos Local

    impl.dart

    @override
    Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() async {
      if (await networkInfo.isConnected) {
        try {
          final remoteTrivia = await remoteDataSource.getRandomNumberTrivia();
          localDataSource.cacheNumberTrivia(remoteTrivia);
          return Right(remoteTrivia);
        } on ServerException {
          return Left(ServerFailure());
        }
      } else {
        try {
          final localTrivia = await localDataSource.getLastNumberTrivia();
          return Right(localTrivia);
        } on CacheException {
          return Left(CacheFailure());
        }
      }
    }

    La única diferencia literal entre concreto y aleatorio es solo pedir que se refactorice este código. La mayor parte de la lógica se puede compartir entre los métodos concretos y aleatorios y manejaremos la única llamada diferente al origen de datos local con una función de orden superior. La versión final de NumberTriviaRepositoryImpl se verá como la siguiente

    impl.dart

    typedef Future<NumberTrivia> _ConcreteOrRandomChooser();
    
    class NumberTriviaRepositoryImpl implements NumberTriviaRepository {
      final NumberTriviaRemoteDataSource remoteDataSource;
      final NumberTriviaLocalDataSource localDataSource;
      final NetworkInfo networkInfo;
    
      NumberTriviaRepositoryImpl({
        @required this.remoteDataSource,
        @required this.localDataSource,
        @required this.networkInfo,
      });
    
      @override
      Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
        int number,
      ) async {
        return await _getTrivia(() {
          return remoteDataSource.getConcreteNumberTrivia(number);
        });
      }
    
      @override
      Future<Either<Failure, NumberTrivia>> getRandomNumberTrivia() async {
        return await _getTrivia(() {
          return remoteDataSource.getRandomNumberTrivia();
        });
      }
    
      Future<Either<Failure, NumberTrivia>> _getTrivia(
        _ConcreteOrRandomChooser getConcreteOrRandom,
      ) async {
        if (await networkInfo.isConnected) {
          try {
            final remoteTrivia = await getConcreteOrRandom();
            localDataSource.cacheNumberTrivia(remoteTrivia);
            return Right(remoteTrivia);
          } on ServerException {
            return Left(ServerFailure());
          }
        } else {
          try {
            final localTrivia = await localDataSource.getLastNumberTrivia();
            return Right(localTrivia);
          } on CacheException {
            return Left(CacheFailure());
          }
        }
      }
    }

    Si esto no es algo bello, ¡no sé qué es! Todas las pruebas aún están aprobadas, por lo que podemos estar seguros de que esta refactorización no rompió nada.

    ¿Qué sigue?

    Ahora que tenemos el repositorio completamente implementado, comenzaremos a trabajar en las partes de bajo nivel de la capa de datos implementando las fuentes de datos y la información de la red.

    Aquí les comparto el video.

     

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

    Subir