말랑한 하루

[Flutter] TDD (Test Driven Development) - DataSource 본문

개발/Flutter

[Flutter] TDD (Test Driven Development) - DataSource

지수는말랑이 2024. 2. 3. 00:12
반응형

※ 글의 순서는 🐇>🥕>🍒>🍇>🍌>🍏 순서로 하위 내용을 구성하고 있습니다.

 

DataSource는 외부 API와 통신을 진행하는 Framework & Driver 계층에 속합니다. 이 계층에서는 HTTP 통신에 대한 객체 생성과 관리, 요청에 대한 테스트 진행 방법에 대해 기술하려 합니다.

 

🥕 todo_remote_data_source_test

이 테스트는 TodoList를 구현할 때, HTTP 통신을 위한 Dio 라이브러리를 활용하여 외부 API와 통신하는 TodoRemoteDataSource Class의 메소드를 검증하는 역할을 수행합니다.

 

🍒 객체 선언 및 초기화

🍇 Create Mock Object of HTTP Client

네트워크 요청을 시뮬레이션 하기 위해서 먼저, DataSource Class에서 전반적으로 사용되는 HTTP 요청 라이브러리인 Dio에 대한 객체를 Mock 객체로 생성합니다.

class MockDioClient extends Mock implements Dio {}

🍇 Initialize Mock Objects

Mock 객체와 테스트 대상 객체에 대한 초기화를 진행합니다.

  late TodoRemoteDataSource dataSource;
  late MockDioClient mockDioClient;

  setUp(() {
    mockDioClient = MockDioClient();
    dataSource = TodoRemoteDataSourceImpl(dio: mockDioClient);
  });

🍒 더미 데이터 생성

HTTP 요청을 통해 반환 될 데이터의 더미 데이터 값을 추가합니다.

final List<Map<String, dynamic>> todoListJson = [
  {"id": 1, "title": "Todo 1"},
  {"id": 2, "title": "Todo 2"},
];

🍒 테스트 선언/준비/실행/검증

🍇 테스트 선언

테스트 이름을 설정합니다. 해당 테스트가 어떤 동작을 검증하는지 쉽게 이해할 수 있도록 작성해야 합니다.

일반적으로 "should get ~ from repository" / "should return ~ when ~"과 같은 형태로 사용됩니다. 테스트가 어떤 결과를 예상하고 있는지 명시적으로 나타낼 수 있습니다.

test('should perform a GET request on the given URL', () async {});

🍇 테스트 준비/실행/검증

테스트는 arrange/act/assert 즉, 준비/실행/검증 단계로 구조화되어 작성됩니다. 코드의 가독성을 높이고 어떤 부분에서 테스트가 실패했는지 명확하게 파악할 수 있습니다.

특히 여러 테스트 케이스를 작성할 때 일관성을 유지하고 테스트의 목적을 명확히 전달하는 것에 도움을 줄 수 있습니다.

test('should perform a GET request on the given URL', () async {
  // arrange
  when(mockDioClient.get(any)).thenAnswer(
    (_) async => Response(data: jsonEncode(todoListJson), statusCode: 200),
  );

  // act
  await dataSource.fetchRemoteTodos();

  // assert
  verify(mockDioClient.get('<https://api.example.com/todos>'));
});

dataSource에서 get요청을 진행할 경우에 대한 테스트를 진행하고 있으므로, 목적에 대한 각 arrange/act/assert는 다음과 같은 모습으로 설계할 수 있습니다.

 

🍌 arrange

"when" 그리고 "thenAnswer" 메소드를 활용하여 HTTP 요청과 요청에 대한 응답을 설정합니다.

설정된 값의 반환은 Response 객체를 생성하여 data와 statusCode를 할당합니다.

 

🍏 thenAnswer

thenAnswer 메소드는 mockito 라이브러리에서 제공하는 기능입니다.

정형화된 코드는 다음과 같습니다.

when(mockObject.methodName(argument)).thenAnswer((_) async => result);

🍌 act

실제 dataSource에서 get요청을 담당하는 함수를 실행합니다. 결과가 존재하거나, 에러가 발생할 것으로 예상하는 경우 새로운 변수에 결과를 할당합니다.

 

🍌 assert

"expact"와 "verify" 메소드를 활용해 테스트가 예상대로 결과를 반환했는지 검증합니다.

 

🍏 expact

flutter_test 라이브러리에 속한 메소드로 예상 결과를 검증할 때 사용됩니다.

actual (테스트 대상)과 matcher (기대 결과 값)을 매개변수로 가지며, 조건을 만족하지 않을 시 테스트를 실패로 표기합니다.

🍏 verify

mockito 라이브러리에 속한 메소드로 Mock 객체의 메소드 호출을 검증할 때 사용됩니다.

 

🍇 테스트 그룹

여러개의 테스트를 그룹화 할 수 있습니다. 관련된 테스트 케이스를 묶어서 Test Suite를 형성하고, 코드의 특정 부분이나 기능에 대한 다양한 측면을 검증하기 용이하게 만들어줍니다.

 

보통 가독성 향상/유사한 행위에 대한 통합 검증/효율적인 테스트 수행/코드 수정 용이성에 대한 이점을 얻을 수 있습니다.

"group" 메소드를 활용하고 전반적인 테스트를 통합하여 표현할 수 있는 문구 또는 함수명을 작성하는 것이 보편적입니다.

 

전체 소스코드는 다음과 같습니다.

class MockDioClient extends Mock implements Dio {}

void main() {
  late TodoRemoteDataSource dataSource;
  late MockDioClient mockDioClient;

  setUp(() {
    mockDioClient = MockDioClient();
    dataSource = TodoRemoteDataSourceImpl(dio: mockDioClient);
  });

  group('fetchRemoteTodos', () {
    final List<map<string, dynamic="">> todoListJson = [
      {"id": 1, "title": "Todo 1"},
      {"id": 2, "title": "Todo 2"},
    ];

    final List todoList = todoListJson
        .map((json) => TodoModel.fromJson(json))
        .toList();

    test('should perform a GET request on the given URL', () async {
      // arrange
      when(mockDioClient.get(any)).thenAnswer(
        (_) async => Response(data: jsonEncode(todoListJson), statusCode: 200),
      );

      // act
      await dataSource.fetchRemoteTodos();

      // assert
      verify(mockDioClient.get('<https://api.example.com/todos>'));
    });

    test('should return a list of TodoModel when the response code is 200', () async {
      // arrange
      when(mockDioClient.get(any)).thenAnswer(
        (_) async => Response(data: jsonEncode(todoListJson), statusCode: 200),
      );

      // act
      final result = await dataSource.fetchRemoteTodos();

      // assert
      expect(result, equals(todoList));
    });

    test('should throw a DioError when the response code is not 200', () {
      // arrange
      when(mockDioClient.get(any)).thenAnswer(
        (_) async => Response(data: 'Not Found', statusCode: 404),
      );

      // act
      final call = dataSource.fetchRemoteTodos;

      // assert
      expect(() => call(), throwsA(isA()));
    });
  });
}

🍒 테스트 진행

테스트는 루트 디렉토리에서 다음 명령어를 활용하여 실행할 수 있습니다.

flutter test test/data/datasources/todo_remote_data_source_test.dart 

각 테스트 케이스에 대한 결과와 통과 여부에 대한 정보가 터미널에 출력됩니다.

00:10 +3: All tests passed!
00:10 +3 -1: Some tests failed.

테스트 결과는 시간/개수/내용으로 구분되어 표기되며 성공한 테스트는 "+", 실패한 테스트는 "-"로 표기됩니다.

 

테스트 코드에서 print 함수를 활용하여 명시적으로 작성한 내용과 함께 실패의 원인이 되는 에러 메세지가 터미널에 표기되므로, 참고하여 테스트 준비/실행/검증 단계를 반복해가면 됩니다.

반응형
Comments