Skip to content

Instantly share code, notes, and snippets.

@DanielCardonaRojas
Last active April 23, 2020 17:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielCardonaRojas/fb931b65253fdf6f3c103499ce318f0b to your computer and use it in GitHub Desktop.
Save DanielCardonaRojas/fb931b65253fdf6f3c103499ce318f0b to your computer and use it in GitHub Desktop.
Repository definition
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) {}
}
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));
}
}
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)),
);
}));
}
}
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()));
});
});
});
}
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(() {}));
}
}
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