[Flutter] : TDD - 09. Remote Data Source
이제 원격 저장소, 즉 API 호출을 통해 데이터를 가져오는 부분을 구현해보겠다.
먼저 테스트 코드!
number_trivia_remote_data_source_test.dart
class MockHttpClient extends Mock implements http.Client {}
void main() {
NumberTriviaRemoteDataSourceImpl dataSource;
MockHttpClient mockHttpClient;
setUp(() {
mockHttpClient = MockHttpClient();
dataSource = NumberTriviaRemoteDataSourceImpl(cline: mockHttpClient);
});
}
Http 라이브러리를 Mock으로 만들어주고
아직 구현되지 않은 RemoteDataSourceImpl에 Parameter로 넣어주었다.
이제 구현!
number_trivia_remote_data_source.dart
class NumberTriviaRemoteDataSourceImpl implements NumberTriviaRemoteDataSource {
final http.Client client;
NumberTriviaRemoteDataSourceImpl({@required this.client});
@override
Future<NumberTriviaModel> getConcreteNumberTrivia(int number) {
// TODO: implement getConcreteNumberTrivia
throw UnimplementedError();
}
@override
Future<NumberTriviaModel> getRandomNumberTrivia() {
// TODO: implement getRandomNumberTrivia
throw UnimplementedError();
}
}
자 이제 구현 준비가 되었다!
함수를 하나씩 살펴보며 진행해보자.
getConcreteNumberTrivia
먼저 NumbersAPI 에서 데이터를 가져오는 부분이다.
[http://numbersapi.com](http://numbersapi.com/42)/42
이런 URL에 header에 Content-Type: application/json
방식으로 GET
리퀘스트를 날리면 데이터가 들어온다. 들어온 데이터는 JSON
으로 처리하자.
먼저 테스트 코드 작성이다!
test.dart
group('getConcreteNumberTrivia', () {
final tNumber = 1;
test(
'should perform a GET request on a URL with number being the endpoint and with application/json header',
() {
// arrange
when(mockHttpClient.get(any, headers: anyNamed('headers'))).thenAnswer(
(realInvocation) async => http.Response(fixture('trivia.json'), 200));
// act
final result = dataSource.getConcreteNumberTrivia(tNumber);
// assert
verify(mockHttpClient.get(
'http://numbersapi.com/$tNumber',
headers: {'Content-Type': 'application/json'},
));
});
});
Mock HTTP로 부터 요청을 보냈을 때 아래 verify에 있는 정상 동작을 하는지 확인한다.
impl.dart
@override
Future<NumberTriviaModel> getConcreteNumberTrivia(int number) {
client.get(
'http://numbersapi.com/$number',
headers: {'Content-Type': 'application/json'},
);
}
테스트가 정상 동작한다.
여기서 arrange 부분이 사실 지워져도 정상 동작한다. 하지만 나중을 위해서 (impl코드에 기능이 추가됨) 저렇게 arrange로 정리해두지 않으면 valid object가 return되어도 오류가 발생할 수 있다. 저 test의 주요 목적은 http가 정상적으로 GET, URL, Headers를 맞췄는지 확인하기 위함에 있다.
다음 테스트다.
http에서 GET 호출에 대한 정상적인 반응은 RESPONSE CODE = 200 이다.
test.dart
final tNumberTriviaModel =
NumberTriviaModel.fromJson(json.decode(fixture('trivia.json')));
test('should return NumberTrivia when the response code is 200 (success)',
() {
// arrange
when(mockHttpClient.get(any, headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response(fixture('trivia.json'), 200));
// act
final result = dataSource.getConcreteNumberTrivia(tNumber);
// assert
expect(result, equals(tNumberTriviaModel));
});
impl.dart
@override
Future<NumberTriviaModel> getConcreteNumberTrivia(int number) async {
final response = await client.get(
'http://numbersapi.com/$number',
headers: {'Content-Type': 'application/json'},
);
return NumberTriviaModel.fromJson(json.decode(response.body));
}
정상 동작한다!
여기서 아직 서버 에러 부분을 처리해주지 않았다.
response_code ≠ 200 인 경우 Exception을 날려주자!
테스트!
test.dart
test(
'should throw a ServerException when the response code is 404 or other',
() async {
// arrange
when(mockHttpClient.get(any, headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response('SomeThing went worng!', 404));
// act
final call = dataSource.getConcreteNumberTrivia;
// assert
expect(() => call(tNumber), throwsA(isInstanceOf<ServerException>()));
});
404코드이거나 다른거 즉, 200코드가 아니면
ServerException을 보내준다.
자 구현!
impl.dart
@override
Future<NumberTriviaModel> getConcreteNumberTrivia(int number) async {
final response = await client.get(
'http://numbersapi.com/$number',
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return NumberTriviaModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
자 정상 동작한다!
여기서 나온 Exception은 Repository에서 ServerFailure로 처리 된다.
DRY Even Inside Tests
DRY (Don’t Repeat Yourself) 원칙에 다라
테스트 내부 코드에서도 중복되는 부분을 줄여보자.
잘 보면 Response Code 가 200일 때와 404 or Other 일 때다.
getConcreteNumberTrivia ⇒ getRandomNumberTrivia 에서 큰 내용 차이가 없다.
한번 테스트 코드 작성해보자.
test.dart
void setUpMockHttpClientSuccess200() {
when(mockHttpClient.get(any, headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response(fixture('trivia.json'), 200));
}
void setUpmockHttpClientFailure404() {
when(mockHttpClient.get(any, headers: anyNamed('headers')))
.thenAnswer((_) async => http.Response('Something went wrong!', 404));
}
group('getRandomNumberTrivia', () {
final tNumberTriviaModel =
NumberTriviaModel.fromJson(json.decode(fixture('trivia.json')));
test(
'should perform a GET request on a URL with *random* endpoint with application/json header',
() {
//arrange
setUpMockHttpClientSuccess200();
// act
dataSource.getRandomNumberTrivia();
// assert
// assert
verify(mockHttpClient.get(
'http://numbersapi.com/random',
headers: {'Content-Type': 'application/json'},
));
});
test('should return NumberTrivia when the response code is 200 (success)',
() async {
// arrange
setUpMockHttpClientSuccess200();
// act
final result = await dataSource.getRandomNumberTrivia();
// assert
expect(result, equals(tNumberTriviaModel));
});
test(
'should throw a ServerException when the response code is 404 or other',
() async {
// arrange
setUpmockHttpClientFailure404();
// act
final call = dataSource.getRandomNumberTrivia;
// assert
expect(() => call(), throwsA(isInstanceOf<ServerException>()));
});
});
impl.dart
@override
Future<NumberTriviaModel> getConcreteNumberTrivia(int number) =>
_getTriviaFromUrl('http://numbersapi.com/$number');
@override
Future<NumberTriviaModel> getRandomNumberTrivia() =>
_getTriviaFromUrl('http://numbersapi.com/random');
Future<NumberTriviaModel> _getTriviaFromUrl(String url) async {
final response = await client.get(
url,
headers: {'Content-Type': 'application/json'},
);
if (response.statusCode == 200) {
return NumberTriviaModel.fromJson(json.decode(response.body));
} else {
throw ServerException();
}
}
impl에서도 중복되는 내용을 함수로 뽑아서
Lambda식으로 정리하였다.
이제 테스트 코드도 정상동작하고 기능도 정상작동한다. 이제 화면에 보여주는 부분들을 하나씩 진행해보자.