Mockito와 테스트 더블 — 의존성을 격리한 테스트 작성

API 호출이나 데이터베이스 접근이 포함된 로직을 테스트할 때, 실제 서버에 요청을 보내면 느리고 불안정합니다. Mock 객체로 의존성을 격리하면 빠르고 안정적인 테스트를 작성할 수 있습니다.


테스트 더블 종류

종류설명예시
Mock호출 검증 + 반환값 설정Mockito Mock
Stub미리 정해진 값을 반환항상 성공 응답
Fake간소화된 구현메모리 DB
Spy실제 객체를 감싸서 호출 추적로깅

설치

YAML
dev_dependencies:
  mockito: ^5.4.0
  build_runner: ^2.4.0

기본 사용법

DART
// 테스트 대상
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;
  }
}
DART
// 테스트 파일
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,
    );
  });
}
BASH
# Mock 클래스 생성
dart run build_runner build

when과 verify

when — 반환값 설정

DART
// 특정 인자에 대한 응답
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 — 호출 검증

DART
// 1번 호출되었는지 확인
verify(mock.getUser(1)).called(1);

// 호출되지 않았는지 확인
verifyNever(mock.saveUser(any));

// 특정 순서로 호출되었는지 확인
verifyInOrder([
  mock.getUser(1),
  mock.saveUser(any),
]);

// 더 이상 호출이 없었는지 확인
verifyNoMoreInteractions(mock);

Fake — 간소화된 구현

DART
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 사용

DART
@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 사용

DART
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);
});

테스트 작성 팁

DART
// 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) 패턴이 테스트 가능한 코드의 핵심입니다
댓글 로딩 중...