말랑한 하루

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

개발/Flutter

[Flutter] TDD (Test Driven Development) - Repository

지수는말랑이 2024. 2. 6. 13:19
반응형

이 테스트는 data폴더에서 구현한 todo_repository를 검증하는 역할을 수행합니다.

repository는 가장 바깥쪽 계층인 Driver, 즉 외부 API와 통신하는 DataSource의 의존성을 주입 받기 때문에, UseCase

🍒 객체 선언 및 초기화

class MockTodoRemoteDataSource extends Mock implements TodoRemoteDataSource {}

void main() {
  late TodoRepository repository;
  late MockTodoRemoteDataSource mockRemoteDataSource;

  setUp(() {
    mockRemoteDataSource = MockTodoRemoteDataSource();
    repository = TodoRepositoryImpl(remoteDataSource: mockRemoteDataSource);
  });

	...
}

🍒 더미 데이터 생성

테스트 성공 사례에서 반환 될 객체에 대한 더미데이터를 생성합니다. 만약 테스트 성공 시 반환 될 결과 값이 존재하지 않는다면 시행하지 않아도 됩니다.

final List<Todo> testTodos = [
  Todo(id: 1, title: 'Todo 1'),
  Todo(id: 2, title: 'Todo 2'),
];

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

🍇 테스트 선언

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

test('should return a list of Todo entities from the remote data source', () {});

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

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

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

test('should return a list of Todo entities from the remote data source', () async {
  // arrange
  when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async => testTodos);

  // act
  final result = await repository.getTodoList();

  // assert
  expect(result, equals(testTodos));
  verify(mockRemoteDataSource.fetchRemoteTodos());
});

현재는 dataSource로부터 최근 Todo의 리스트를 반환받는 테스트를 진행하고 있으므로, 목적에 대한 각 arrange/act/assert는 다음과 같은 모습으로 설계할 수 있습니다.

 

🍌 arrange

Mock을 활용하여 생성한 mockRemoteDataSource 객체에서 Todo에 대한 최근 목록을 불러오는 함수를 실행합니다.

 

그에 대한 결과는 "mockito" 테스트 라이브러리에서 제공하는 "thenAnswer" 메소드를 활용하여 이전에 생성한 더미데이터를 항상 반환하도록 합니다.

 

🍌 act

Repository를 활용해서 최근 Todo 목록을 불러옵니다. 이 방법은, Repository가 정상적으로 내부 getTodoList 메소드를 통해 remoteDataSource에 접근하고 fetchRemoteTodos를 실행하는지 판단합니다.

 

🍌 assert

arrange와 act에서 생성한 두 결과에 대해 검증합니다.

우리는 mockRemoteDataSource가 정상적으로 실행되는지, mock과 repository의 결과가 동일한지 검증을 해야 합니다.

 

mockRemoteDataSource의 경우 mock 객체의 메소드 호출을 검증해야 하므로 mockito 라이브러리의 verify를 사용하고, mock과 repository에 대한 결과를 비교하기 위해 flutter_test 라이브러리의 expact를 사용합니다. verify와 expact는 다음과 같습니다.

 

🍏 expact

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

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

🍏 verify

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

 

🍇 테스트 그룹

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

 

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

 

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

group('getTodoList', () {
	final List<Todo> testTodos = [
	  Todo(id: 1, title: 'Todo 1'),
	  Todo(id: 2, title: 'Todo 2'),
	];
	
	test('should return a list of Todo entities from the remote data source', () async {
	  // arrange
	  when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async => testTodos);
	
	  // act
	  final result = await repository.getTodoList();
	
	  // assert
	  expect(result, equals(testTodos));
	  verify(mockRemoteDataSource.fetchRemoteTodos());
	});
	
	test('should throw an exception if the remote data source call fails', () async {
	  // arrange
	  when(mockRemoteDataSource.fetchRemoteTodos()).thenThrow(Exception('Server error'));
	
	  // act
	  final call = repository.getTodoList;
	
	  // assert
	  expect(() => call(), throwsException);
	  verify(mockRemoteDataSource.fetchRemoteTodos());
	});
	
	test('should throw a specific exception if the remote data source returns an empty list', () async {
	  // arrange
	  when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async => []);
	
	  // act
	  final call = repository.getTodoList;
	
	  // assert
	  expect(() => call(), throwsA(isA<EmptyListException>()));
	  verify(mockRemoteDataSource.fetchRemoteTodos());
	});
});

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

class MockTodoRemoteDataSource extends Mock implements TodoRemoteDataSource {}

void main() {
  late TodoRepository repository;
  late MockTodoRemoteDataSource mockRemoteDataSource;

  setUp(() {
    mockRemoteDataSource = MockTodoRemoteDataSource();
    repository = TodoRepositoryImpl(remoteDataSource: mockRemoteDataSource);
  });

  group('getTodoList', () {
    final List<Todo> testTodos = [
      Todo(id: 1, title: 'Todo 1'),
      Todo(id: 2, title: 'Todo 2'),
    ];

    test('should return a list of Todo entities from the remote data source', () async {
      // arrange
      when(mockRemoteDataSource.fetchRemoteTodos()).thenAnswer((_) async => testTodos);

      // act
      final result = await repository.getTodoList();

      // assert
      expect(result, equals(testTodos));
      verify(mockRemoteDataSource.fetchRemoteTodos());
    });
  });
}

🍒 테스트 진행

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

flutter test {filePath}/{fileName}.dart

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

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

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

 

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

반응형
Comments