말랑한 하루

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

개발/Flutter

[Flutter] TDD (Test Driven Development) - Riverpod

지수는말랑이 2024. 2. 8. 11:59
반응형

이 테스트는 상태관리를 검증하는 역할을 수행합니다.

상태관리를 검증하는 이유는, 코드의 동작을 확인하고 안정성을 보장하여 예측 가능한 애플리케이션을 만들어가기 위해서 진행합니다.

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

반응형
Comments