테스트 기초 — Unit Test, Widget Test, Integration Test
테스트 기초 — Unit Test, Widget Test, Integration Test
Flutter는 세 가지 레벨의 테스트를 지원합니다. 면접에서 "테스트를 어떻게 작성하시나요?"라는 질문에 이 세 가지를 구분해서 답할 수 있어야 합니다.
테스트 피라미드
╱╲
╱ ╲ Integration Test (적게, 느리지만 전체 검증)
╱────╲
╱ ╲ Widget Test (중간, UI 단위 검증)
╱────────╲
╱ ╲ Unit Test (많이, 빠르고 가벼움)
╱────────────╲
| 레벨 | 대상 | 속도 | 비중 |
|---|---|---|---|
| Unit Test | 함수, 클래스, 모델 | 매우 빠름 | 가장 많이 |
| Widget Test | 개별 위젯 | 빠름 | 중간 |
| Integration Test | 전체 앱 흐름 | 느림 | 적게 |
Unit Test
비즈니스 로직, 모델, 유틸리티 함수를 테스트합니다.
// lib/models/calculator.dart
class Calculator {
double add(double a, double b) => a + b;
double subtract(double a, double b) => a - b;
double divide(double a, double b) {
if (b == 0) throw ArgumentError('0으로 나눌 수 없습니다');
return a / b;
}
}
// test/models/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/models/calculator.dart';
void main() {
late Calculator calculator;
// 각 테스트 전에 실행
setUp(() {
calculator = Calculator();
});
group('Calculator', () {
test('덧셈이 정상 동작한다', () {
expect(calculator.add(2, 3), equals(5));
expect(calculator.add(-1, 1), equals(0));
});
test('뺄셈이 정상 동작한다', () {
expect(calculator.subtract(5, 3), equals(2));
});
test('0으로 나누면 에러가 발생한다', () {
expect(
() => calculator.divide(10, 0),
throwsA(isA<ArgumentError>()),
);
});
test('나눗셈이 정상 동작한다', () {
expect(calculator.divide(10, 2), equals(5));
});
});
}
실행
# 전체 테스트 실행
flutter test
# 특정 파일만
flutter test test/models/calculator_test.dart
# 커버리지 포함
flutter test --coverage
Widget Test
개별 위젯의 렌더링과 인터랙션을 테스트합니다.
// lib/widgets/counter_widget.dart
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$_count', key: const Key('counter_text')),
ElevatedButton(
key: const Key('increment_button'),
onPressed: () => setState(() => _count++),
child: const Text('증가'),
),
],
);
}
}
// test/widgets/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/widgets/counter_widget.dart';
void main() {
testWidgets('카운터 위젯 테스트', (WidgetTester tester) async {
// 위젯 빌드
await tester.pumpWidget(
const MaterialApp(home: Scaffold(body: CounterWidget())),
);
// 초기값 확인
expect(find.text('0'), findsOneWidget);
// 버튼 클릭
await tester.tap(find.byKey(const Key('increment_button')));
await tester.pump(); // 리빌드 대기
// 값 증가 확인
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
});
testWidgets('텍스트가 올바르게 표시된다', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: Text('안녕하세요')),
),
);
expect(find.text('안녕하세요'), findsOneWidget);
expect(find.byType(Text), findsOneWidget);
});
}
위젯 찾기 (Finder)
find.text('텍스트'); // 텍스트로 찾기
find.byType(ElevatedButton); // 타입으로 찾기
find.byKey(const Key('my_key')); // Key로 찾기
find.byIcon(Icons.add); // 아이콘으로 찾기
find.byWidgetPredicate((widget) => // 조건으로 찾기
widget is Text && widget.data == '검색');
인터랙션
await tester.tap(finder); // 탭
await tester.longPress(finder); // 롱프레스
await tester.drag(finder, offset); // 드래그
await tester.enterText(finder, '입력'); // 텍스트 입력
await tester.pump(); // 1프레임 리빌드
await tester.pumpAndSettle(); // 애니메이션 완료까지
Matcher
expect(finder, findsOneWidget); // 1개 발견
expect(finder, findsNothing); // 발견 안 됨
expect(finder, findsNWidgets(3)); // N개 발견
expect(finder, findsAtLeastNWidgets(1)); // 최소 N개
Integration Test
전체 앱을 실제 디바이스/에뮬레이터에서 테스트합니다.
# pubspec.yaml
dev_dependencies:
integration_test:
sdk: flutter
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('로그인 → 홈 화면 흐름 테스트', (tester) async {
app.main();
await tester.pumpAndSettle();
// 이메일 입력
await tester.enterText(
find.byKey(const Key('email_field')),
'test@email.com',
);
// 비밀번호 입력
await tester.enterText(
find.byKey(const Key('password_field')),
'password123',
);
// 로그인 버튼 탭
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle();
// 홈 화면 도달 확인
expect(find.text('홈'), findsOneWidget);
});
}
# 실행
flutter test integration_test/app_test.dart
비동기 테스트
test('API에서 데이터를 가져온다', () async {
final service = ApiService();
final result = await service.fetchPosts();
expect(result, isNotEmpty);
expect(result.first.title, isNotEmpty);
});
정리
- Unit Test: 로직, 모델, 유틸 (가장 많이, 가장 빠르게)
- Widget Test: 개별 위젯 렌더링과 인터랙션
- Integration Test: 전체 앱 흐름 (실제 디바이스에서)
find로 위젯을 찾고,expect로 결과를 검증합니다pump()은 1프레임,pumpAndSettle()은 애니메이션 완료까지 대기합니다- Key를 활용하면 테스트에서 위젯을 쉽게 찾을 수 있습니다
댓글 로딩 중...