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.
Curso TDD Arquitectura Limpia
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;
}
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.
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.
Deja una respuesta
Lo siento, debes estar conectado para publicar un comentario.