테스트 기초 — Unit Test, Widget Test, Integration Test

Flutter는 세 가지 레벨의 테스트를 지원합니다. 면접에서 "테스트를 어떻게 작성하시나요?"라는 질문에 이 세 가지를 구분해서 답할 수 있어야 합니다.


테스트 피라미드

PLAINTEXT
        ╱╲
       ╱  ╲         Integration Test (적게, 느리지만 전체 검증)
      ╱────╲
     ╱      ╲       Widget Test (중간, UI 단위 검증)
    ╱────────╲
   ╱          ╲     Unit Test (많이, 빠르고 가벼움)
  ╱────────────╲
레벨대상속도비중
Unit Test함수, 클래스, 모델매우 빠름가장 많이
Widget Test개별 위젯빠름중간
Integration Test전체 앱 흐름느림적게

Unit Test

비즈니스 로직, 모델, 유틸리티 함수를 테스트합니다.

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

실행

BASH
# 전체 테스트 실행
flutter test

# 특정 파일만
flutter test test/models/calculator_test.dart

# 커버리지 포함
flutter test --coverage

Widget Test

개별 위젯의 렌더링과 인터랙션을 테스트합니다.

DART
// 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)

DART
find.text('텍스트');                    // 텍스트로 찾기
find.byType(ElevatedButton);           // 타입으로 찾기
find.byKey(const Key('my_key'));       // Key로 찾기
find.byIcon(Icons.add);               // 아이콘으로 찾기
find.byWidgetPredicate((widget) =>     // 조건으로 찾기
    widget is Text && widget.data == '검색');

인터랙션

DART
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

DART
expect(finder, findsOneWidget);        // 1개 발견
expect(finder, findsNothing);          // 발견 안 됨
expect(finder, findsNWidgets(3));      // N개 발견
expect(finder, findsAtLeastNWidgets(1)); // 최소 N개

Integration Test

전체 앱을 실제 디바이스/에뮬레이터에서 테스트합니다.

YAML
# pubspec.yaml
dev_dependencies:
  integration_test:
    sdk: flutter
DART
// 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);
  });
}
BASH
# 실행
flutter test integration_test/app_test.dart

비동기 테스트

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를 활용하면 테스트에서 위젯을 쉽게 찾을 수 있습니다
댓글 로딩 중...