말랑한 하루
[Flutter] TDD (Test Driven Development) - Riverpod 본문
이 테스트는 상태관리를 검증하는 역할을 수행합니다.
상태관리를 검증하는 이유는, 코드의 동작을 확인하고 안정성을 보장하여 예측 가능한 애플리케이션을 만들어가기 위해서 진행합니다.
🍒 Notifier 구현
Riverpod의 Notifier를 활용한 상태관리를 구현하는 부분을 추가합니다. Notifier는 TodoUseCase를 활용하여 Todo데이터를 State에 저장하고 관리합니다.
final todoProvider = StateNotifierProvider<TodoNotifier, AsyncValue<List<Todo>>>((ref) {
final todoUseCase = ref.read(todoUseCaseProvider);
return TodoNotifier(todoUseCase: todoUseCase);
});
class TodoNotifier extends StateNotifier<AsyncValue<List<Todo>>> {
final TodoUseCase todoUseCase;
TodoNotifier({required this.todoUseCase}) : super(AsyncValue.loading()) {
fetchTodoList();
}
Future<void> fetchTodoList() async {
state = AsyncValue.loading();
try {
final todos = await todoUseCase(NoParams());
state = AsyncValue.data(todos);
} catch (_) {
state = AsyncValue.error('Failed to fetch todos');
}
}
}
🍒 객체 선언 및 초기화
Riverpod의 경우, Provider를 활용하기 때문에 ProviderContainer와 비즈니스 로직인 UseCase가 필요합니다. 이후 container의 Provider에 UseCase의 결과 값을 추가합니다.
이 준비를 함으로써, 우리는 Provider를 활용하는 Container의 동작에 대해 검증하고 상태관리가 정확히 진행되는 지 파악할 수 있습니다.
class MockTodoUseCase extends Mock implements TodoUseCase {}
void main() {
late ProviderContainer container;
late MockTodoUseCase mockTodoUseCase;
setUp(() {
container = ProviderContainer();
mockTodoUseCase = MockTodoUseCase();
when(mockTodoUseCase(NoParams())).thenAnswer((_) async => [
Todo(id: 1, title: 'Todo 1'),
Todo(id: 2, title: 'Todo 2'),
]);
container.readProvider.overrideWithProvider(todoUseCaseProvider.overrideWithValue(mockTodoUseCase));
});
...
}
🍒 테스트 선언/준비/실행/검증
🍇 테스트 선언
테스트 이름을 설정합니다. 해당 테스트가 어떤 동작을 검증하는지 쉽게 이해할 수 있도록 작성해야 합니다.
test('should emit data state when fetchTodoList succeeds', () async {});
🍇 테스트 준비/실행/검증
테스트는 arrange/act/assert 즉, 준비/실행/검증 단계로 구조화되어 작성됩니다. 코드의 가독성을 높이고 어떤 부분에서 테스트가 실패했는지 명확하게 파악할 수 있습니다.
특히 여러 테스트 케이스를 작성할 때 일관성을 유지하고 테스트의 목적을 명확히 전달하는 것에 도움을 줄 수 있습니다.
test('initial state should be loading', () {
expect(container.read(todoProvider), equals(const AsyncValue.loading()));
});
상태관리 테스트에서는 이전과는 다른 새로운 테스트도 있습니다. 위 코드는 Container가 정확히 로딩 중인지 상태를 확인하는 테스트입니다.
test('should emit data state when fetchTodoList succeeds', () async {
// act
await container.read(todoProvider.notifier).fetchTodoList();
// assert
expect(container.read(todoProvider), equals(const AsyncValue.data([
Todo(id: 1, title: 'Todo 1'),
Todo(id: 2, title: 'Todo 2'),
])));
});
test('should emit error state when fetchTodoList fails', () async {
// arrange
when(mockTodoUseCase(NoParams())).thenThrow(Exception('Failed to fetch todos'));
// act
await container.read(todoProvider.notifier).fetchTodoList();
// assert
expect(container.read(todoProvider), equals(const AsyncValue.error('Failed to fetch todos')));
});
위 두 예제는 Provider의 상태 값과, fetch를 통해 결과 값으로 생성한 TodoList의 값이 동일한지 판단하고 fetch에 실패했을 경우에 대한 결과입니다.
🍇 테스트 그룹
여러개의 테스트를 그룹화 할 수 있습니다. 관련된 테스트 케이스를 묶어서 Test Suite를 형성하고, 코드의 특정 부분이나 기능에 대한 다양한 측면을 검증하기 용이하게 만들어줍니다.
보통 가독성 향상/유사한 행위에 대한 통합 검증/효율적인 테스트 수행/코드 수정 용이성에 대한 이점을 얻을 수 있습니다.
"group" 메소드를 활용하고 전반적인 테스트를 통합하여 표현할 수 있는 문구 또는 함수명을 작성하는 것이 보편적입니다.
전체 소스코드는 다음과 같습니다.
class MockTodoUseCase extends Mock implements TodoUseCase {}
void main() {
late ProviderContainer container;
late MockTodoUseCase mockTodoUseCase;
setUp(() {
container = ProviderContainer();
mockTodoUseCase = MockTodoUseCase();
when(mockTodoUseCase(NoParams())).thenAnswer((_) async => [
Todo(id: 1, title: 'Todo 1'),
Todo(id: 2, title: 'Todo 2'),
]);
container.readProvider.overrideWithProvider(todoUseCaseProvider.overrideWithValue(mockTodoUseCase));
});
group('TodoNotifier', () {
test('initial state should be loading', () {
expect(container.read(todoProvider), equals(const AsyncValue.loading()));
});
test('should emit data state when fetchTodoList succeeds', () async {
// act
await container.read(todoProvider.notifier).fetchTodoList();
// assert
expect(container.read(todoProvider), equals(const AsyncValue.data([
Todo(id: 1, title: 'Todo 1'),
Todo(id: 2, title: 'Todo 2'),
])));
});
test('should emit error state when fetchTodoList fails', () async {
// arrange
when(mockTodoUseCase(NoParams())).thenThrow(Exception('Failed to fetch todos'));
// act
await container.read(todoProvider.notifier).fetchTodoList();
// assert
expect(container.read(todoProvider), equals(const AsyncValue.error('Failed to fetch todos')));
});
});
}
🍒 테스트 진행
테스트는 루트 디렉토리에서 다음 명령어를 활용하여 실행할 수 있습니다.
flutter test {filePath}/{fileName}.dart
각 테스트 케이스에 대한 결과와 통과 여부에 대한 정보가 터미널에 출력됩니다.
00:10 +3: All tests passed!
00:10 +3 -1: Some tests failed.
테스트 결과는 시간/개수/내용으로 구분되어 표기되며 성공한 테스트는 "+", 실패한 테스트는 "-"로 표기됩니다.
테스트 코드에서 print 함수를 활용하여 명시적으로 작성한 내용과 함께 실패의 원인이 되는 에러 메세지가 터미널에 표기되므로, 참고하여 테스트 준비/실행/검증 단계를 반복해가면 됩니다.
'개발 > Flutter' 카테고리의 다른 글
[Flutter] Infinity Carousel (무한 캐러셀) (0) | 2024.02.23 |
---|---|
[Flutter] Infinity Scroll (무한 스크롤) (0) | 2024.02.20 |
[Flutter] TDD (Test Driven Development) - UseCase (0) | 2024.02.07 |
[Flutter] TDD (Test Driven Development) - Repository (0) | 2024.02.06 |
[Flutter] TDD (Test Driven Development) - DataSource (0) | 2024.02.03 |