[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식으로 정리하였다.

이제 테스트 코드도 정상동작하고 기능도 정상작동한다. 이제 화면에 보여주는 부분들을 하나씩 진행해보자.