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

flutterclean6
Compartir

 

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

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!

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

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.

 

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.

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.

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.

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

 

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).

 

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.

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.

 


Compartir

Entradas relacionadas

Deja tu comentario