클린아키텍처를 사용하는 영화 정보 앱 구현 중,
HomeViewModel에는 4가지 메서드와 그에 따른 상태를 담았다. 이 뷰모델을 테스트할 때는, 기존에 배운 방법을 활용했다.
기존의 방법(흐름)
[사전 설정]
1. 테스트용 Mock 클래스 생성(실제 Usecase를 대체하는 가짜 클래스)
2. main 함수 내부에서 ProviderContainer 선언
3. setUp 함수 내부에서 UsecaseProvider를 Mock객체로 오버라이드
4. setUp함수 내부에서 providerContainer 를 초기화, 오버라이드는 3에서 만든 가짜 usecase로 설정
[실제 테스트]
1. when문 조건: providerContainer에서 usecaseProvider를 읽어와 execute를 실행함
2. thenAnswer: 가짜 데이터를 주고, 그 데이터를 사용해 메서드를 비동기 호출.
기존에 알던 방법을 적용한 프로젝트와 현재 구현중인 맵이 크게 다르지 않아 똑같이 적용하면 될 것이라고 생각하고 코드를 작성했으나, 테스트를 통과할 수 없었다.
여러 위치에서 print문으로 콘솔 메세지를 찍어 보자,

실행 전과 실행 후의 값이 양쪽 다 Null로 같은 것을 확인할 수 있었다.
메서드를 실행하기 전에도, 실행한 후에도 값이 똑같다는 점에서 예상한 문제는
1. 메서드가 값을 반환하지 않는다.(메서드 구현의 문제)
2. 메서드를 제대로 불러오지 못했다
3. 테스트용 가짜 데이터를 불러오지 못했다
이 세가지였는데, 1번은 테스트하려는 코드를 다시 살펴본 결과 문제가 없는 듯 했다.
2번은 vs코드가 바로 잡아줄 것이기 때문에 크게 고려하지 않았다.
따라서 3번을 예상하고, 계속해서 코드를 검토한 결과,
뷰모델 provider가 build() 시점에서 usecase들을 자동으로 호출하기 때문에, usecase 호출이
when, thenAnswer문보다 먼저 실행된다는 것을 발견했다.
즉,
테스트하려는 fetch... 메서드는 이미 실행되었는데
그 이후에 가짜 데이터를 대신 넣어주려고 하는 when, thenAnswer문이 실행되어서 메서드가 가짜 데이터 없이 null을 반환한 것이다.
UsecaseProvider를 setUp함수, 즉 초기화 시점이 아닌 테스트코드에 넣는 것으로 이 문제를 해결할 수 있었다.
최종 코드
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:movie_info_app/domain/entity/movie.dart';
import 'package:movie_info_app/domain/usecase/fetch_now_playing_movies_usecase.dart';
import 'package:movie_info_app/domain/usecase/fetch_popular_movies_usecase.dart';
import 'package:movie_info_app/domain/usecase/fetch_top_rated_movies_usecase.dart';
import 'package:movie_info_app/domain/usecase/fetch_upcoming_movies_usecase.dart';
import 'package:movie_info_app/presentation/pages/home/home_view_model.dart';
import 'package:movie_info_app/presentation/providers.dart';
class MockNowPlayingUsecase extends Mock
implements FetchNowPlayingMoviesUsecase {}
class MockPopularUsecase extends Mock implements FetchPopularMoviesUsecase {}
class MockTopRatedUsecase extends Mock implements FetchTopRatedMoviesUsecase {}
class MockUpcomingUsecase extends Mock implements FetchUpcomingMoviesUsecase {}
late MockNowPlayingUsecase mockNowPlayingUsecase;
late MockPopularUsecase mockPopularUsecase;
late MockTopRatedUsecase mockTopRatedUsecase;
late MockUpcomingUsecase mockUpcomingUsecase;
void main() {
late ProviderContainer providerContainer;
setUp(() {
// 기존에 UsecaseProvider 를 호출하던 위치 (문제 원인)
//
mockNowPlayingUsecase = MockNowPlayingUsecase();
mockPopularUsecase = MockPopularUsecase();
mockTopRatedUsecase = MockTopRatedUsecase();
mockUpcomingUsecase = MockUpcomingUsecase();
});
// fetchNowPlayingMovies test
test(
'HomeViewModel test : state update after fetchNowPlayingMovies',
() async {
when(
() => mockNowPlayingUsecase.execute(),
).thenAnswer((_) async => [Movie(id: 123, posterPath: "posterPath")]);
// UsecaseProvider를 테스트 함수 내부로 옮겨 호출되는 순서 문제를 해결
providerContainer = ProviderContainer(
overrides: [
fetchNowPlayingMoviesUsecaseProvider.overrideWith(
(ref) => mockNowPlayingUsecase,
),
],
);
// 실행 전 상태 확인
final stateBefore = providerContainer.read(homeViewModelProvider);
expect(stateBefore.nowPlaying, isNull);
// 실행
await providerContainer
.read(homeViewModelProvider.notifier)
.fetchNowPlayingMovies();
// 실행 후 상태 확인
final stateAfter = providerContainer.read(homeViewModelProvider);
expect(stateAfter.nowPlaying, isNotNull);
expect(stateAfter.popular, isNull);
expect(stateAfter.nowPlaying!.length, 1);
expect(stateAfter.nowPlaying!.first.id, 123);
addTearDown(providerContainer.dispose);
},
);
}'Flutter' 카테고리의 다른 글
| [Flutter] Mocktail 패키지로 api 메서드 테스트하기 (0) | 2025.05.13 |
|---|---|
| [Flutter] dotenv로 api키 안전하게 관리하기 (0) | 2025.05.12 |
| [Flutter] ListView.separated 주석 해석 (0) | 2025.05.08 |
| [Flutter] 화면 크기 구하는 코드 (0) | 2025.05.01 |
| [Flutter] 플러터의 기본적인 암시적 애니메이션 위젯 (0) | 2025.05.01 |

