Last active
April 23, 2020 17:59
-
-
Save DanielCardonaRojas/fb931b65253fdf6f3c103499ce318f0b to your computer and use it in GitHub Desktop.
Repository definition
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:dartz/dartz.dart'; | |
abstract class Synchronizable extends Identifiable { | |
SyncronizedMark get syncStatus; | |
} | |
enum SyncronizedMark { synchronized, needsCreation, needsUpdate, needsDeletion } | |
class CacheFirstRepository<Entity extends Synchronizable> | |
implements Repository<Entity> { | |
final Repository<Entity> remoteRepository; | |
final Repository<Entity> cacheRepository; | |
Future<Either<Failure, void>> synchronize() {} | |
CacheFirstRepository(this.remoteRepository, this.cacheRepository); | |
/// Adds new entity | |
/// | |
/// Tries remote if succeeds, marks synchronized and caches | |
/// if remote fails marks for creation and caches | |
/// fails if cache fails | |
@override | |
Future<Either<Failure, void>> add(Entity entity) {} | |
/// Delete entity | |
/// | |
/// Tries remote if succeeds deletes from cache | |
/// otherwise updates entity marking for deletion | |
@override | |
Future<Either<Failure, void>> delete(Entity entity) {} | |
/// Get all entities | |
/// | |
/// Tries remote if succeeds, then cache | |
/// If remote fails fallback to cache result. | |
@override | |
Future<Either<Failure, List<Entity>>> getAll() {} | |
/// Get entity by id | |
/// | |
/// If remote succeeds the entity is cached. | |
/// If remote fails fallback to cache result. | |
@override | |
Future<Either<Failure, Entity>> getById(UniqueId id) {} | |
/// Update entity | |
/// | |
/// Tries remote is succeeds then cache marking syncronized | |
/// if remote fails update entity marking needs update | |
@override | |
Future<Either<Failure, void>> update(Entity entity) {} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:dartz/dartz.dart'; | |
import 'package:rideshare/application/interfaces/repository.dart'; | |
import 'package:rideshare/domain/domain.dart'; | |
import 'package:rideshare/domain/entities/identifiable.dart'; | |
import 'package:rideshare/utilities/error/failures.dart'; | |
class InMemoryRepository<E extends Identifiable> implements Repository<E> { | |
final Map<String, E> entitySet; | |
static const void unit = null; | |
InMemoryRepository._(this.entitySet); | |
factory InMemoryRepository.fromList(List<E> entities) { | |
final map = | |
Map<String, E>.fromEntries(entities.map((e) => MapEntry(e.id, e))); | |
return InMemoryRepository._(map); | |
} | |
@override | |
Future<Either<Failure, void>> add(E entity) async { | |
entitySet[entity.id] = entity; | |
return Future.value(Right(unit)); | |
} | |
@override | |
Future<Either<Failure, void>> delete(E entity) { | |
entitySet.remove(entity); | |
return Future.value(Right(unit)); | |
} | |
@override | |
Future<Either<Failure, List<E>>> getAll() { | |
final list = entitySet.values.toList(); | |
return Future.value(Right(list)); | |
} | |
@override | |
Future<Either<Failure, E>> getById(UniqueId id) { | |
final entity = entitySet[id.value]; | |
return Future.value(Right(entity)); | |
} | |
@override | |
Future<Either<Failure, void>> update(E entity) { | |
entitySet[entity.id] = entity; | |
return Future.value(Right(unit)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class NetworkConnectivityFailure extends Failure {} | |
class RemoteFirstRepository<Entity extends Identifiable> | |
implements Repository<Entity> { | |
final Repository<Entity> remoteRepository; | |
final Repository<Entity> cacheRepository; | |
final NetworkInfo networkChecker; | |
RemoteFirstRepository({ | |
@required this.networkChecker, | |
@required this.remoteRepository, | |
@required this.cacheRepository, | |
}); | |
static final Task<Either<Failure, dynamic>> connectivityFailureTask = | |
Task(() => Future.value(Left(NetworkConnectivityFailure()))); | |
/// Adds new entity | |
/// | |
/// Fails if remote fails. Caches only when remote succeeds. | |
@override | |
Future<Either<Failure, void>> add(Entity entity) { | |
final task = Task(() => remoteRepository.add(entity)).bindEither((_) { | |
return Task(() => cacheRepository.add(entity)); | |
}); | |
return Task(() => networkChecker.isConnected) | |
.flatMap((isConnected) => isConnected ? task : connectivityFailureTask) | |
.run(); | |
} | |
/// Delete entity | |
/// | |
/// Fails is remote fails. | |
/// Deletes from cache only when first deleted from remote | |
@override | |
Future<Either<Failure, void>> delete(Entity entity) { | |
final task = Task(() => remoteRepository.delete(entity)).bindEither((_) { | |
return Task(() => cacheRepository.delete(entity)); | |
}); | |
return Task(() => networkChecker.isConnected) | |
.flatMap((isConnected) => isConnected ? task : connectivityFailureTask) | |
.run(); | |
} | |
/// Get all entities | |
/// | |
/// If remote succeeds results are cached. | |
/// If remote fails fallback to cache result. | |
@override | |
Future<Either<Failure, List<Entity>>> getAll() { | |
final cacheTask = Task(() => cacheRepository.getAll()); | |
final remoteTask = Task(() => remoteRepository.getAll()); | |
final task = remoteTask.bindEither((list) { | |
return Task(() async { | |
for (final entity in list) { | |
await cacheRepository.add(entity); | |
} | |
return Right(list); | |
}); | |
}).orDefault(cacheTask); | |
return Task(() => networkChecker.isConnected) | |
.flatMap((hasConnection) => hasConnection ? task : cacheTask) | |
.run(); | |
} | |
/// Get entity by id | |
/// | |
/// If remote succeeds the entity is cached. | |
/// If remote fails fallback to cache result. | |
@override | |
Future<Either<Failure, Entity>> getById(UniqueId id) { | |
final cacheTask = Task(() => cacheRepository.getById(id)); | |
final remoteTask = Task(() => remoteRepository.getById(id)); | |
final task = remoteTask.bindEither((entity) { | |
return Task(() async { | |
await cacheRepository.add(entity); | |
return Right(entity); | |
}); | |
}).orDefault(cacheTask); | |
return Task(() => networkChecker.isConnected) | |
.flatMap((hasConnection) => hasConnection ? task : cacheTask) | |
.run(); | |
} | |
/// Update entity | |
/// Fails on any remote failure | |
/// Caches only when remote update succeeds | |
@override | |
Future<Either<Failure, void>> update(Entity entity) { | |
final task = Task(() => remoteRepository.update(entity)).bindEither((_) { | |
return Task(() => cacheRepository.update(entity)); | |
}); | |
return Task(() => networkChecker.isConnected) | |
.flatMap((isConnected) => isConnected ? task : connectivityFailureTask) | |
.run(); | |
} | |
} | |
extension TaskEitherMonad<T> on Task<Either<Failure, T>> { | |
Task<Either<Failure, A>> bindEither<A>( | |
Function1<T, Task<Either<Failure, A>>> f) { | |
return bind((eitherT) => Task(() { | |
return eitherT.fold( | |
(failure) => Future.value(Left(failure)), | |
(valueT) => f(valueT).run(), | |
); | |
})); | |
} | |
} | |
extension TaskEitherAlternative<T> on Task<Either<Failure, T>> { | |
Task<Either<Failure, T>> orDefault(Task<Either<Failure, T>> task) { | |
return bind((eitherT) => Task(() { | |
return eitherT.fold( | |
(failure) => task.run(), | |
(valueT) => Future.value(Right(valueT)), | |
); | |
})); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Todo extends Equatable implements Identifiable { | |
final String note; | |
final String id; | |
Todo({ | |
@required this.note, | |
@required this.id, | |
}); | |
@override | |
List<Object> get props => [note, id]; | |
} | |
class MockRemoteRepository extends Mock implements Repository<Todo> {} | |
class MockLocalRepository extends Mock implements Repository<Todo> {} | |
class MockNetworkInfo extends Mock implements NetworkInfo {} | |
void main() { | |
RemoteFirstRepository sut; | |
MockRemoteRepository mockRemoteRepository; | |
MockLocalRepository mockLocalRepository; | |
MockNetworkInfo mockNetworkChecker; | |
final listEquality = ListEquality().equals; | |
final tEntity = Todo(id: '0', note: ''); | |
final tUniqueId = UniqueId(''); | |
final tEntities = [ | |
Todo(id: '0', note: ''), | |
Todo(id: '0', note: ''), | |
Todo(id: '0', note: ''), | |
]; | |
setUp(() { | |
mockRemoteRepository = MockRemoteRepository(); | |
mockLocalRepository = MockLocalRepository(); | |
mockNetworkChecker = MockNetworkInfo(); | |
sut = RemoteFirstRepository( | |
networkChecker: mockNetworkChecker, | |
cacheRepository: mockLocalRepository, | |
remoteRepository: mockRemoteRepository); | |
}); | |
// ======================== Online tests ======================== | |
group('Device is online', () { | |
setUp(() { | |
when(mockNetworkChecker.isConnected).thenAnswer((_) async => true); | |
}); | |
group('add operation', () { | |
test('fails when remote repository fails', () async { | |
//arrange | |
when(mockRemoteRepository.add(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
final result = await sut.add(tEntity); | |
expect(result, Left(ServerFailure())); | |
}); | |
test('does not call cache when remote repository fails', () async { | |
when(mockRemoteRepository.add(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
await sut.add(tEntity); | |
verify(mockRemoteRepository.add(tEntity)); | |
verifyZeroInteractions(mockLocalRepository); | |
}); | |
test('calls cache when remote repository succeeds', () async { | |
when(mockRemoteRepository.add(any)) | |
.thenAnswer((_) async => Right(tEntity)); | |
await sut.add(tEntity); | |
verify(mockLocalRepository.add(tEntity)); | |
}); | |
}); | |
group('delete operation', () { | |
test('fails when remote repository fails', () async { | |
//arrange | |
when(mockRemoteRepository.delete(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
final result = await sut.delete(tEntity); | |
expect(result, Left(ServerFailure())); | |
}); | |
test('does not call cache when remote repository fails', () async { | |
when(mockRemoteRepository.delete(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
await sut.delete(tEntity); | |
verify(mockRemoteRepository.delete(tEntity)); | |
verifyZeroInteractions(mockLocalRepository); | |
}); | |
test('calls cache when remote repository succeeds', () async { | |
when(mockRemoteRepository.delete(any)) | |
.thenAnswer((_) async => Right(() {})); | |
await sut.delete(tEntity); | |
verify(mockLocalRepository.delete(tEntity)); | |
}); | |
}); | |
group('update operation', () { | |
test('fails when remote repository fails', () async { | |
//arrange | |
when(mockRemoteRepository.update(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
final result = await sut.update(tEntity); | |
expect(result, Left(ServerFailure())); | |
}); | |
test('does not call cache when remote repository fails', () async { | |
when(mockRemoteRepository.update(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
await sut.update(tEntity); | |
verify(mockRemoteRepository.update(tEntity)); | |
verifyZeroInteractions(mockLocalRepository); | |
}); | |
test('calls cache when remote repository succeeds', () async { | |
when(mockRemoteRepository.update(any)) | |
.thenAnswer((_) async => Right(() {})); | |
await sut.update(tEntity); | |
verify(mockLocalRepository.update(tEntity)); | |
}); | |
}); | |
// READ OPERATIONS | |
group('get all operation', () { | |
test('falls back to cache when remote fails', () async { | |
when(mockRemoteRepository.getAll()) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
when(mockLocalRepository.getAll()) | |
.thenAnswer((_) async => Right(tEntities)); | |
final result = await sut.getAll(); | |
final entities = result.fold((_) => [], (v) => v); | |
assert(result.isRight()); | |
assert(listEquality(entities, tEntities)); | |
verify(mockRemoteRepository.getAll()); | |
verify(mockLocalRepository.getAll()); | |
}); | |
test( | |
'caches every entity gotten from remote when remote fetch is succesful', | |
() async { | |
when(mockRemoteRepository.getAll()) | |
.thenAnswer((_) async => Right(tEntities)); | |
final result = await sut.getAll(); | |
assert(result.isRight()); | |
verify(mockLocalRepository.add(any)).called(tEntities.length); | |
}); | |
}); | |
group('get by id', () { | |
test('falls back to cache when remote fails', () async { | |
when(mockRemoteRepository.getById(any)) | |
.thenAnswer((_) async => Left(ServerFailure())); | |
when(mockLocalRepository.getById(any)) | |
.thenAnswer((_) async => Right(tEntity)); | |
final result = await sut.getById(tUniqueId); | |
expect(result, Right(tEntity)); | |
verify(mockRemoteRepository.getById(tUniqueId)); | |
verify(mockLocalRepository.getById(tUniqueId)); | |
}); | |
test( | |
'caches every entity gotten from remote when remote fetch is succesful', | |
() async { | |
when(mockRemoteRepository.getById(any)) | |
.thenAnswer((_) async => Right(tEntity)); | |
final result = await sut.getById(tUniqueId); | |
assert(result.isRight()); | |
verify(mockLocalRepository.add(any)).called(1); | |
}); | |
}); | |
}); | |
// ======================== Offline tests ======================== | |
group('Device is offline', () { | |
setUp(() { | |
when(mockNetworkChecker.isConnected).thenAnswer((_) async => false); | |
}); | |
group('get by id', () { | |
test('calls cache directly if internet connectivity is down', () async { | |
when(mockLocalRepository.getById(any)) | |
.thenAnswer((_) async => Right(tEntity)); | |
final result = await sut.getById(tUniqueId); | |
expect(result, Right(tEntity)); | |
verify(mockLocalRepository.getById(tUniqueId)); | |
verifyZeroInteractions(mockRemoteRepository); | |
}); | |
}); | |
group('get all', () { | |
test('calls cache directly if internet connectivity is down', () async { | |
when(mockLocalRepository.getAll()) | |
.thenAnswer((_) async => Right(tEntities)); | |
final result = await sut.getAll(); | |
verify(mockLocalRepository.getAll()); | |
verifyZeroInteractions(mockRemoteRepository); | |
}); | |
}); | |
group('add operation', () { | |
test('fails withouth calling remote when connectiviy is down', () async { | |
final result = await sut.add(tEntity); | |
verifyZeroInteractions(mockRemoteRepository); | |
expect(result, Left(NetworkConnectivityFailure())); | |
}); | |
}); | |
group('delete operation', () { | |
test('fails withouth calling remote when connectiviy is down', () async { | |
final result = await sut.delete(tEntity); | |
verifyZeroInteractions(mockRemoteRepository); | |
expect(result, Left(NetworkConnectivityFailure())); | |
}); | |
}); | |
group('update operation', () { | |
test('fails withouth calling remote when connectiviy is down', () async { | |
final result = await sut.update(tEntity); | |
verifyZeroInteractions(mockRemoteRepository); | |
expect(result, Left(NetworkConnectivityFailure())); | |
}); | |
}); | |
}); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:dartz/dartz.dart'; | |
import 'package:domain/domain.dart'; | |
import 'package:domain/entities/identifiable.dart'; | |
import 'package:utilities/error/failures.dart'; | |
abstract class Repository<EntityType extends Identifiable> { | |
Future<Either<Failure, EntityType>> getById(UniqueId id); | |
Future<Either<Failure, List<EntityType>>> getAll(); | |
Future<Either<Failure, void>> add(EntityType entity); | |
Future<Either<Failure, void>> update(EntityType entity); | |
Future<Either<Failure, void>> delete(EntityType entity); | |
} | |
class InMemoryRepository<E extends Identifiable> implements Repository<E> { | |
final Map<String, E> entitySet; | |
InMemoryRepository._(this.entitySet); | |
factory InMemoryRepository.fromList(List<E> entities) { | |
final map = | |
Map<String, E>.fromEntries(entities.map((e) => MapEntry(e.id, e))); | |
return InMemoryRepository._(map); | |
} | |
@override | |
Future<Either<Failure, void>> add(E entity) async { | |
entitySet[entity.id] = entity; | |
return Future.value(Right(() {})); | |
} | |
@override | |
Future<Either<Failure, void>> delete(E entity) { | |
entitySet.remove(entity); | |
return Future.value(Right(() {})); | |
} | |
@override | |
Future<Either<Failure, List<E>>> getAll() { | |
final list = entitySet.values.toList(); | |
return Future.value(Right(list)); | |
} | |
@override | |
Future<Either<Failure, E>> getById(UniqueId id) { | |
final entity = entitySet[id.value]; | |
return Future.value(Right(entity)); | |
} | |
@override | |
Future<Either<Failure, void>> update(E entity) { | |
entitySet[entity.id] = entity; | |
return Future.value(Right(() {})); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:clean_architecture_template/application/interfaces/repository.dart'; | |
import 'package:clean_architecture_template/domain/entities/identifiable.dart'; | |
import 'package:mockito/mockito.dart'; | |
import 'package:meta/meta.dart'; | |
class RepositorySpy<Entity> extends Mock implements Repository<Entity> { | |
Repository<Entity> realRepository; | |
void Function(Repository<Entity>) clearAction; | |
RepositorySpy({@required this.realRepository, this.clearAction}) { | |
configure(); | |
} | |
/// Call this on tearDown configuration of tests | |
void tearDown() { | |
if (clearAction != null) { | |
clearAction(realRepository); | |
} else { | |
realRepository.clear(); | |
} | |
} | |
void configure() { | |
when(add(any)).thenAnswer((invocation) => | |
realRepository.add(invocation.positionalArguments.first as Entity)); | |
when(update(any)).thenAnswer((invocation) => | |
realRepository.update(invocation.positionalArguments.first as Entity)); | |
when(delete(any)).thenAnswer((invocation) => | |
realRepository.delete(invocation.positionalArguments.first as Entity)); | |
when(getAll()).thenAnswer((_) => realRepository.getAll()); | |
when(getById(any)).thenAnswer((invocation) => realRepository | |
.getById(invocation.positionalArguments.first as UniqueId)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment