[Flutter] : TDD - 06. Repository Implementation
전 시간에 이어서 이제 Repository TDD를 완성해보자.
지난 시간
지난 시간에 Parameter받는 부분까지 완성했었다.
test/…/data/repositories/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,
);
});
}
data/repositories/number_trivia_repository_impl.dart
class NumberTriviaRepositoryImpl extends 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;
}
}
자 이제 기능별로 Repository를 구현해보자.
1. getConcreteNumberTrivia
여기서 해야 할 일을 먼저 정의해보자.
- API로 부터 데이터 가져오기
- 유저 network가 offline이면 cache로 부터 데이터 가져오기
- API 데이터 cache로 등록하기
1-1. NetworkInfo
먼저 network에 따라 분기가 되니 그 부분 test코드를 작성하자.
NetworkInfo를 Mocking해서 일단 isConnected가 불리도록 해보자.
test/…/repositories/number_trivia_repository_impl_test.dart
group('getConcreteNumberTrivia', () {
// 데이터 가져오기 혹은 에러 발생
final tNumber = 1;
final tNumberTriviaModel =
NumberTriviaModel(text: 'test trivia', number: tNumber);
final NumberTrivia tNumberTrivia = tNumberTriviaModel;
test('should check if the device is offline', () {
// arrange
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
// act
repository.getConcreteNumberTrivia(tNumber);
// assert
verify(mockNetworkInfo.isConnected);
});
});
이제 Test 코드를 작성했으니 구현해보도록 하자.
data/repositories/number_trivia_repository_impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(int number) {
networkInfo.isConnected;
return null;
}
임시로 구현해보았다.
Test는 성공적으로 통과한다.
1-2. getConcreteNumberTrivia
잘 보면 network 연결에 따라 비즈니스 로직이 분기 된다.
이에 따라 test코드를 작성해보자.
먼저 online부터 시작하자.
test/…/repositories/number_trivia_impl_test.dart
group('device is online', () {
setUp(() {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
test(
'should return remote data when the call to remote data source is successful',
() async {
when(mockRemoteDataSource.getConcreteNumberTrivia(tNumber))
.thenAnswer((_) async => tNumberTriviaModel);
final result = await repository.getConcreteNumberTrivia(tNumber);
verify(mockRemoteDataSource.getConcreteNumberTrivia(tNumber));
expect(result, equals(tNumberTrivia));
});
});
});
data/repositories/number_trivia_repository_impl.dart
`@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number) async {
networkInfo.isConnected;
return Right(await remoteDataSource.getConcreteNumberTrivia(number));
}
구현은 완벽하진 않지만 테스트 동작만 돌아가게 작성해보았다.
test 기반이기 때문에 먼저 test부터 제대로 작성하자.
1-3. cacheNumberTrivia
이제 cache부분 test코드를 작성해주자.
test/…/repositories/number_trivia_impl_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(tNumberTriviaModel));
});
cache부분과 get부분이 잘 호출되는지 확인하는 test코드이다.
간단히 구현해보자.
…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);
}
cache에 데이터 넣어주는 부분을 추가했다.
정상적으로 테스트를 잘 통과한다.
1-4. ServerException
이제는 온라인 데이터를 가져오다가 ServerException이 발생했을 때 test 코드를 작성해보자.
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에서 Exception처리를 해주자.
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());
}
}
try on 문을 이용해 처리해줬다.
테스트를 돌려보니 잘 동작한다.
1-5. getLastNumberTrivia
이제 오프라인 동작 테스트를 작성해보자.
먼저 NetworkInfo에서 isConnected가 false로 셋팅하고 테스트 코드를 작성해보자.
…/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 {
// arrage
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를 수정해보자.
impl.dart
@override
Future<Either<Failure, NumberTrivia>> getConcreteNumberTrivia(
int number) async {
networkInfo.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);
}
}
1-6. cache exception
cache 데이터를 가져올 때 exception 에러가 발생 할 수 있다. 그 부분에 대해서 test 코드를 작성하자.
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 {
networkInfo.isConnected;
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());
}
}
}
2. getRandomNumberTrivia
이제 드디어 다음으로 Random Trivia를 처리하기 전에
잠깐 코드를 살펴보자.
크게 2가지 패턴이 보인다. 온라인, 오프라인.
그리고 Concrete, Random
이걸 잘 나눠서 묶어서 처리해보자.
test.dart
void runTestOnline(Function body) {
group('device is online', () {
setUp(() async {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => true);
});
body();
});
}
void runTestOffline(Function body) {
group('device is offline', () {
setUp(() async {
when(mockNetworkInfo.isConnected).thenAnswer((_) async => false);
});
});
}
원래 TDD 방식대로라면 항상 테스트를 하나씩 하면서 구현해야한다. 이제 나오는 getRandomNumberTrivia에만 위 함수를 적용해보자.
test.dart
group('getRandomNumberTrivia', () {
final tNumberTriviaModel =
NumberTriviaModel(text: 'test trivia', number: 123);
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);
});
runTestOnline(() {
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())));
});
});
runTestOffline(() {
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())));
});
});
});
기존 코드와 거의 비슷하기 때문에 조금씩만 수정하면서 진행하면 된다.
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());
}
}
}
이렇게 구현해 보니 concrete와 random에서 사실 거의 차이가 없다.
차이 점은 함수 호출을 어떤걸 하느냐인데 이 부분을 refactoring 해보자.
impl.dart
typedef Future<NumberTrivia> _ConcreteOrRandomChooser();
class NumberTriviaRepositoryImpl extends 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());
}
}
}
}
공통 함수로 묶어서 처리하도록 하였다.
결론
초록 불 마음이 행복해진다.
이제 Repository 구현은 끝이 났다.
다음으로 데이터를 가져오는 부분, 네트워크 확인 부분을 진행해보자.