Mockito와 테스트 더블 — 의존성을 격리한 테스트 작성
Mockito와 테스트 더블 — 의존성을 격리한 테스트 작성
API 호출이나 데이터베이스 접근이 포함된 로직을 테스트할 때, 실제 서버에 요청을 보내면 느리고 불안정합니다. Mock 객체로 의존성을 격리하면 빠르고 안정적인 테스트를 작성할 수 있습니다.
테스트 더블 종류
| 종류 | 설명 | 예시 |
|---|---|---|
| Mock | 호출 검증 + 반환값 설정 | Mockito Mock |
| Stub | 미리 정해진 값을 반환 | 항상 성공 응답 |
| Fake | 간소화된 구현 | 메모리 DB |
| Spy | 실제 객체를 감싸서 호출 추적 | 로깅 |
설치
dev_dependencies:
mockito: ^5.4.0
build_runner: ^2.4.0
기본 사용법
// 테스트 대상
abstract class UserRepository {
Future<User> getUser(int id);
Future<List<User>> getAllUsers();
Future<void> saveUser(User user);
}
class UserService {
final UserRepository _repository;
UserService(this._repository);
Future<String> getUserName(int id) async {
final user = await _repository.getUser(id);
return user.name;
}
}
// 테스트 파일
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
// Mock 클래스 자동 생성
@GenerateMocks([UserRepository])
import 'user_service_test.mocks.dart';
void main() {
late MockUserRepository mockRepository;
late UserService userService;
setUp(() {
mockRepository = MockUserRepository();
userService = UserService(mockRepository);
});
test('getUserName이 올바른 이름을 반환한다', () async {
// Given: Mock 설정
when(mockRepository.getUser(1)).thenAnswer(
(_) async => const User(id: 1, name: '심정훈'),
);
// When: 실행
final name = await userService.getUserName(1);
// Then: 검증
expect(name, equals('심정훈'));
verify(mockRepository.getUser(1)).called(1);
});
test('에러 발생 시 예외를 전파한다', () async {
when(mockRepository.getUser(any)).thenThrow(
Exception('네트워크 에러'),
);
expect(
() => userService.getUserName(1),
throwsException,
);
});
}
# Mock 클래스 생성
dart run build_runner build
when과 verify
when — 반환값 설정
// 특정 인자에 대한 응답
when(mock.getUser(1)).thenAnswer((_) async => user);
// 모든 인자에 대한 응답
when(mock.getUser(any)).thenAnswer((_) async => user);
// 에러 발생
when(mock.getUser(999)).thenThrow(NotFoundException());
// 연속 호출 시 다른 응답
when(mock.getUser(1))
.thenAnswer((_) async => user1) // 첫 번째 호출
.thenAnswer((_) async => user2); // 두 번째 호출
verify — 호출 검증
// 1번 호출되었는지 확인
verify(mock.getUser(1)).called(1);
// 호출되지 않았는지 확인
verifyNever(mock.saveUser(any));
// 특정 순서로 호출되었는지 확인
verifyInOrder([
mock.getUser(1),
mock.saveUser(any),
]);
// 더 이상 호출이 없었는지 확인
verifyNoMoreInteractions(mock);
Fake — 간소화된 구현
class FakeUserRepository extends Fake implements UserRepository {
final _users = <int, User>{};
@override
Future<User> getUser(int id) async {
final user = _users[id];
if (user == null) throw NotFoundException();
return user;
}
@override
Future<void> saveUser(User user) async {
_users[user.id] = user;
}
@override
Future<List<User>> getAllUsers() async {
return _users.values.toList();
}
}
// 테스트에서 사용
test('저장 후 조회가 동작한다', () async {
final fakeRepo = FakeUserRepository();
final service = UserService(fakeRepo);
await fakeRepo.saveUser(const User(id: 1, name: '심정훈'));
final name = await service.getUserName(1);
expect(name, equals('심정훈'));
});
Widget Test에서 Mock 사용
@GenerateMocks([PostRepository])
import 'post_list_test.mocks.dart';
void main() {
testWidgets('게시글 목록이 표시된다', (tester) async {
final mockRepo = MockPostRepository();
when(mockRepo.getAllPosts()).thenAnswer(
(_) async => [
const Post(id: 1, title: '첫 번째 글'),
const Post(id: 2, title: '두 번째 글'),
],
);
await tester.pumpWidget(
MaterialApp(
home: PostListScreen(repository: mockRepo),
),
);
// 로딩 완료 대기
await tester.pumpAndSettle();
expect(find.text('첫 번째 글'), findsOneWidget);
expect(find.text('두 번째 글'), findsOneWidget);
});
}
Provider와 함께 Mock 사용
testWidgets('Provider와 Mock 조합', (tester) async {
final mockService = MockApiService();
when(mockService.getItems()).thenAnswer(
(_) async => [Item(name: '테스트')],
);
await tester.pumpWidget(
Provider<ApiService>.value(
value: mockService,
child: const MaterialApp(home: ItemListScreen()),
),
);
await tester.pumpAndSettle();
expect(find.text('테스트'), findsOneWidget);
});
테스트 작성 팁
// 1. Arrange-Act-Assert (AAA) 패턴
test('사용자 삭제 테스트', () async {
// Arrange (준비)
when(mockRepo.deleteUser(1)).thenAnswer((_) async {});
// Act (실행)
await userService.deleteUser(1);
// Assert (검증)
verify(mockRepo.deleteUser(1)).called(1);
});
// 2. 테스트 이름은 한국어로 명확하게
test('로그인 실패 시 에러 메시지를 반환한다', () async { ... });
// 3. 하나의 테스트는 하나의 동작만 검증
// 나쁜 예: 생성 + 조회 + 삭제를 한 테스트에서
// 좋은 예: 각각 별도 테스트로 분리
정리
- Mock 은 호출 검증과 반환값 설정에, Fake 는 간소화된 구현이 필요할 때 사용합니다
@GenerateMocks로 Mock 클래스를 자동 생성합니다when으로 Mock의 동작을 설정하고,verify로 호출을 검증합니다- AAA(Arrange-Act-Assert) 패턴으로 테스트를 구조화하세요
- 의존성 주입(DI) 패턴이 테스트 가능한 코드의 핵심입니다
댓글 로딩 중...