1. 왜 테스트 코드를 작성해야 할까?
기능을 추가하거나 기존 기능을 변경하더라도 앱이 계속 작동하도록 하려면 어떻게 해야 할까요?
바로 테스트 코드를 작성하는 것입니다.
예를 들어 로그인 화면에 새로운 기능을 추가했는데 기존 로그인 기능이 작동하지 않는다면 큰 문제겠죠?
하지만 테스트 코드가 있으면 문제를 바로 알 수 있기 때문에 기능을 수정하거나 추가해도 기존 기능을 안전하게 유지할 수 있습니다.
또한, 아직 해보지는 않았지만 GitHub CI/CD 자동화 기능을 사용하면 코드를 올릴 때 테스트가 자동으로 실행되도록 설정할 수도 있다고 합니다.
이렇게 하면 코드 변경 시마다 사람이 직접 확인하지 않아도 앱이 잘 작동하는 지 자동으로 검증할 수 있습니다.
물론 초반에는 테스트 코드를 작성하는 것이 번거로울 수 있지만,
프로젝트의 규모가 커질수록 테스트 코드는 버그를 빠르게 찾고 고치는 데 드는 시간을 줄일 수 있습니다.
2. Flutter 테스트 종류
Flutter에서는 다양한 부분을 테스트할 수 있도록 세 가지 테스트 방식을 지원합니다.
1) 단위 테스트 (Unit Test) - 함수나 클래스처럼 가장 작은 단위를 테스트
2) 위젯 테스트 (Widget Test) - UI 위젯이 올바르게 작동하는지 확인
3) 통합 테스트 (Integration Test) - 전체 앱 흐름으르 실제처럼 테스트
3. Unit Test (단위 테스트)
단위 테스트는 단일 함수, 메서드 또는 클래스 등 작은 코드 단위를 독립적으로 테스트하는 것입니다.
단위 테스트의 목표는 다양한 조건에서 논리 단위의 정확성을 검증하는 것입니다.
1) add 함수 테스트
지금부터 간단한 add 함수를 테스트하겠습니다.
// calculator.dart
int add(int a, int b) {
return a + b;
}
// calculator_test.dart
void main() {
test('add test', () => expect(add(2, 3), 5));
}
Run을 클릭하면 테스트가 실행되고,
테스팅 결과가 정상적으로 성공하면 아래와 같이 체크가 표시됩니다.

로그창에서도 테스트 실행 결과를 확인할 수 있습니다.

2) 테스트 실패

테스트가 실패하면 어떤 부분에서 실패하는지를 보여줍니다.
3) 테스트 코드 알아보기
// calculator_test.dart
void main() {
test('add test', () => expect(add(2, 3), 5));
}
① test('add test', ...)
테스트를 정의하는 함수로 두 개의 인자를 받습니다.
첫 번째 인자는 테스트 이름으로 이 테스트가 무엇을 테스트하는지를 설명합니다.
저는 add test라고 했는데요. 이를 로그창에서 확인할 수 있습니다.

두 번째 인자는 테스트 내용으로 실제 검증 로직이 들어갑니다.
② expect(add(2, 3), 5)
expect를 한국어로 직역하면 '예상하다'입니다.
이와 같이 실제 결과가 기대한 값과 같은지를 비교하는 역할을 합니다.
이 함수도 두 개의 인자를 받는데요.
첫 번째 인자는 실제 실행할 코드가 오며,
여기서는 실행할 함수인 add(2, 3)이 옵니다.
두 번째 인자는 첫 번째 인자로 넣은 코드가 실행됐을 때 기대하는 값이 오는데요.
add(2, 3)을 실행했을 때 기대하는 5를 넣었습니다.
만약 결과가 5가 아니라면 테스트는 실패하고, 실패 메시지를 출력합니다.
③ group
이 외에도 관련된 테스트들을 묶을 수 있는 group을 제공합니다.
void main() {
group('add test', () {
test('양수 더하기', () => expect(add(2, 3), 5));
test('0 더하기', () => expect(add(0, 1), 1));
test('음수 더하기', () => expect(add(-2, -1), -3));
});
}
group을 사용함으로써 깔끔하고 읽기 쉬운 테스트 구조를 만들 수 있습니다.
4) Counter 클래스 Unit Test
이제 본격적으로 단위 테스트를 진행하겠습니다.
① 파일 생성
먼저 두개의 파일을 생성합니다.
일반적으로 테스트 파일은 test 폴더 안에 있어야 하며, 항상 _test.dart로 끝나야 합니다.
counter_app/
lib/
counter.dart
test/
counter_test.dart
② 테스트할 클래스 생성
다음으로 테스트할 단위(Unit)가 필요합니다.
단위는 함수, 메서드 또는 클래스의 다른 이름으로, 이 예제에서는 Counter 클래스를 생성합니다.
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
③ 테스트 코드 작성
counter_test.dart 파일 내부에 테스트 코드를 작성합니다.
test와 expect를 사용하여 결과가 올바른지 확인합니다.
import 'package:counter_app/counter.dart';
import 'package:test/test.dart';
void main() {
test('Counter value should be incremented', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
}
④ group으로 여러 테스트 진행
import 'package:counter_app/counter.dart';
import 'package:test/test.dart';
void main() {
group('Test start, increment, decrement', () {
test('value should start at 0', () {
expect(Counter().value, 0);
});
test('value should be incremented', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
test('value should be decremented', () {
final counter = Counter();
counter.decrement();
expect(counter.value, -1);
});
});
}
4. Widget Test (위젯 테스트)
위젯 테스트는 하나의 UI 위젯을 테스트하는 것으로
해당 위젯이 예상대로 화면에 표시되고, 버튼을 눌렀을 때 올바르게 반응하는지를 확인하는 것이 목표입니다.
실제 앱처럼 위젯을 테스트하기 위해서는 Flutter의 위젯 생명주기를 흉내내는 테스트 환경이 필요합니다.
그래서 테스트할 때는 WidgetTester라는 툴을 통해 가짜 UI 환경을 통해
그 안에서 위젯을 화면에 띄우고, 클릭하거나 텍스트를 입력하는 등의 테스트를 진행할 수 있습니다.
1) 테스트 대상 위젯
테스트 대상 위젯은 텍스트, 버튼, 입력창 같은 UI 요소입니다.
이러한 위젯은 사용자 이벤트(클릭, 입력)에 반응하고, 레이아웃을 다시 그리거나,
내부에 자식 위젯들을 포함할 수도 있습니다.
2) Unit Test와 차이점
단위 테스트는 계산 함수, 로직 등 코드의 일부분을 테스트하는 것이고,
위젯 테스트는 실제로 UI 위젯이 화면에 어떻게 보이고 반응하는지를 테스트합니다.
그렇기 때문에 위젯 테스트는 범위가 더 넓고 복잡합니다.
3) testWidgets()
위젯 테스트에서는 test 함수가 아닌 testWidget함수를 사용하여 테스트를 진행합니다.
testWidgets(
String description,
WidgetTesterCallback callback,
);
testWidget도 두개의 인자를 받는데요.
첫 번째 인자는 테스트가 무엇을 확인하는지를 설명하는 문자열입니다.
이 부분은 Unit Test의 test 함수와 비슷하죠?
두 번째 인자는 calback 함수로 실제 테스트 코드가 들어갑니다.
인자로 WidgetTester tester를 받으며, tester는 테스트 환경에서 위젯을 화면에 띄우고, 클릭하고, 검사할 수 있습니다.
4) Widget Test 예제
① 테스트 위젯 생성
title과 message를 표시하는 위젯을 생성합니다.
class MyWidget extends StatelessWidget {
const MyWidget({super.key, required this.title, required this.message});
final String title;
final String message;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: Text(message)),
),
);
}
}
② 테스트 코드 작성
위젯 테스트를 시작하기 위해 testWidget() 함수를 사용합니다.
테스트 환경에서 MyWidget을 실제로 그리기 위해 tester.pumpWidget()을 사용합니다.
void main() {
testWidgets('MyWidget has a title and message', (tester) async {
await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
});
}
pumpWidget()은 주어진 위젯을 테스트 화면에 실제로 띄우는 역할을 하며,
title에 T, message에 M을 넘겨서 화면에 해당 텍스트들이 보이는지 확인합니다.
③ 위젯 검색
위젯트리에서 Finder를 사용하여 title, message 텍스트 위젯을 검색합니다.
Finder는 테스트 환경에서 화면에 어떤 위젯이 있는지를 찾는 도구입니다.
void main() {
testWidgets('MyWidget has a title and message', (tester) async {
await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
final titleFinder = find.text('T');
final messageFinder = find.text('M');
});
}
find.Text('T')는 화면에 T라는 텍스트가 들어간 Text 위젯을 찾고,
find.Text('M')는 화면에 M이라는 텍스트가 들어간 Text 위젯을 찾습니다.
④ Matcher를 사용해 위젯 검증하기
찾은 위젯이 실제로 화면에 존재하는지, 몇 번 나타나는지를 확인하기 위해 Matcher를 사용합니다.
findsOneWidget은 화면에 정확히 하나의 해당 위젯이 존재해야 한다는 뜻입니다.
void main() {
testWidgets('MyWidget has a title and message', (tester) async {
await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
final titleFinder = find.text('T');
final messageFinder = find.text('M');
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
});
}
| Matcher | 의미 |
| findsOneWidget | 정확히 1개 존재해야 함 |
| findsNothing | 하나도 없어야 함 |
| findsWidgets | 1개 이상 있어야 함 |
| findsNWidgets(n) | 정확히 n개 있어야 함 |
| matchesGoldenFile() | 이미지 스냅샷과 비교 (화면 테스트) |
5. Integration Test (통합 테스트)
통합 테스트는 앱 전체 흐름을 테스트합니다.
통합 테스트의 목표는 테스트 대상인 모든 위젯과 서비스가 예시대로 함께 작동하는지 확인하는 것이며,
이를 통해 앱의 성능을 검증할 수 있습니다.
일반적으로 통합 테스트는 실제 기기나 iOS 시뮬레이터 / Android 애뮬레이터와 같은 OS 애뮬레이터에서 실행됩니다.
1) Unit Test, Widget Test의 차이점
Unit Test는 '이 코드(함수, 클래스)가 제대로 작동하는가?'를 테스트하고,
Widget Test는 '이 버튼이 화면에 보이고 눌렀을 때 반응하는가?'를 테스트하며,
Integration Test는 '앱 전체가 사용자 흐름대로 문제 없이 작동하는가?'를 테스트합니다.
2) Counter App Integration Test 예제
① integration_test 패키지 설치
pubspec.yaml 파일에 integration_test에 대한 종속성을 추가합니다.
dependencies:
flutter:
sdk: flutter
integration_test:
sdk: flutter
② 테스트할 새 앱 생성
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Counter App',
home: MyHomePage(title: 'Counter App Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment'),
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
③ 통합 테스트 파일 생성
counter_app/
lib/
main.dart
integration_test/
app_test.dart
④ 통합 테스트 작성
통합 테스트에 사용되는 테스트 코드들은 위젯 테스트와 유닛 테스트에서 사용한 방법을 결합해서 구성하면 됩니다.
import 'package:basic_counter/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
// 통합 테스트를 위해 Flutter 내부에서 테스트 환경 준비
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// group으로 테스트 정의
// end-to-end test는 앱 전체 흐름 테스트라는 뜻
group('end-to-end test', () {
// 실제 테스트 정의
// tester는 위젯을 화면에 띄우고 누르고 확인할 수 있는 테스트 도구
testWidgets('tap on the floating action button, verify counter', (
tester,
) async {
// MyApp 위젯을 실제로 테스트 환경에 띄움
await tester.pumpWidget(const MyApp());
// 앱 실행 시 화면에 '0'이라는 텍스트가 정확히 한 번 표시되는지 확인
expect(find.text('0'), findsOneWidget);
// ValueKey('increment')를 가진 위젯을 찾음
final fab = find.byKey(const ValueKey('increment'));
// 찾은 버튼을 터치
await tester.tap(fab);
// 화면이 완전히 다시 그려질 때까지 기다림
await tester.pumpAndSettle();
// 버튼을 누른 뒤, 카운터가 1로 증가했는지 확인
expect(find.text('1'), findsOneWidget);
});
});
}
⑤ Android 기기에서 테스트
통합 테스트를 실행하기 위해서는 실제 디바이스나 시뮬레이터에서 실행해야 하기 때문에 터미널에서 아래 명령어를 실행합니다.
flutter test integration_test/app_test.dart
결과는 다음과 같이 출력되어야합니다.
flutter test integration_test/app_test.dart
00:04 +0: loading /path/to/counter_app/integration_test/app_test.dart
00:15 +0: loading /path/to/counter_app/integration_test/app_test.dart
00:18 +0: loading /path/to/counter_app/integration_test/app_test.dart 2,387ms
Installing build/app/outputs/flutter-apk/app.apk... 612ms
00:21 +1: All tests passed!
테스트가 완료된 후에는 카운터 앱이 제거되었는지 확인합니다.
완료 후 앱이 남아 있으면 수동으로 삭제해주는 것이 좋습니다.
⑥ iOS 기기에서 테스트
flutter test integration_test/app_test.dart
결과는 다음과 같이 출력되어야 합니다.
flutter test integration_test/app_test.dart
00:04 +0: loading /path/to/counter_app/integration_test/app_test.dart
00:15 +0: loading /path/to/counter_app/integration_test/app_test.dart
00:18 +0: loading /path/to/counter_app/integration_test/app_test.dart 2,387ms
Xcode build done. 13.5s
00:21 +1: All tests passed!
테스트가 완료된 후에는 카운터 앱이 제거되었는지 확인합니다.
위 테스트 코드를 실행하면 버튼을 클릭하지 않아도 자동으로 테스트 해주는 모습을 확인할 수 있습니다.
6. 참고자료
https://docs.flutter.dev/cookbook/testing/unit/introduction
https://docs.flutter.dev/cookbook/testing
https://docs.flutter.dev/testing/overview
'Flutter' 카테고리의 다른 글
| [Flutter] 기능 우선 분리(Feature first Structure)로 영화관 앱 구조 설계하기 (0) | 2025.05.15 |
|---|---|
| [Flutter] mocktail을 이용하여 Unit Test 하기 (mockito vs mocktail) (0) | 2025.05.08 |
| [Flutter] Riverpod으로 상태관리하고, MVVM패턴 적용해서 Counter앱 만들기 (0) | 2025.05.02 |
| [Flutter] API 키 숨기기 (feat. dotenv) (0) | 2025.04.22 |
| [Flutter] 지역 검색 앱 프로젝트 소개 (0) | 2025.04.22 |