티스토리 뷰

Flutter 애플리케이션 개발 시, 사용자와의 상호작용은 앱의 핵심 요소 중 하나입니다. 따라서 탭, 드래그, 텍스트 입력과 같은 제스처가 올바르게 동작하는지 테스트하는 것은 매우 중요합니다. 이번 블로그에서는 이러한 상호작용을 테스트하는 방법과 주요 기능에 대해 자세히 알아보겠습니다.

참고. Tap, drag, and enter text

위젯 테스트의 중요성

위젯 테스트는 Flutter의 고유한 테스트로, 격리된 방식으로 각 위젯을 테스트할 수 있습니다. 이를 통해 앱의 UI와 상호작용을 검증하며, 사용자 경험을 향상시킬 수 있습니다. 위젯 테스트는 앱을 빌드하고 실행하는 과정 없이 바로 테스트가 가능하며, UI 요소의 동작을 검증할 수 있습니다.

Flutter에서의 상호작용 테스트 설정하기

Flutter에서는 flutter_test 패키지를 사용하여 위젯 테스트를 작성할 수 있습니다. 이를 위해 pubspec.yaml 파일의 dev_dependencies 섹션에 flutter_test 종속성을 포함해야 합니다.

dev_dependencies:
  flutter_test:
    sdk: flutter

주요 상호작용 테스트 메서드

WidgetTester 클래스는 다양한 상호작용을 시뮬레이션할 수 있는 메서드를 제공합니다:

  • tap: 특정 위젯에 대한 탭 동작을 시뮬레이션합니다.
  • drag: 특정 위치에서 드래그 동작을 시뮬레이션합니다.
  • enterText: 텍스트 필드에 텍스트 입력을 시뮬레이션합니다.

상호작용 테스트 예제

아래는 버튼을 탭하고, 드래그 동작을 수행하며, 텍스트를 입력하는 테스트 예제입니다:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('탭, 드래그, 텍스트 입력 테스트', (WidgetTester tester) async {
    // 테스트용 앱 위젯 생성
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('테스트 앱')),
        body: Column(
          children: [
            ElevatedButton(
              onPressed: () {},
              child: Text('탭 버튼'),
            ),
            GestureDetector(
              onPanUpdate: (details) {},
              child: Container(
                key: Key('드래그 박스'),
                width: 100,
                height: 100,
                color: Colors.blue,
              ),
            ),
            TextField(
              key: Key('텍스트 필드'),
            ),
          ],
        ),
      ),
    ));

    // '탭 버튼'을 찾고 탭 동작 시뮬레이션
    final tapButtonFinder = find.text('탭 버튼');
    await tester.tap(tapButtonFinder);
    await tester.pump();

    // '드래그 박스'를 찾고 드래그 동작 시뮬레이션
    final dragBoxFinder = find.byKey(Key('드래그 박스'));
    await tester.drag(dragBoxFinder, Offset(0, 100));
    await tester.pump();

    // '텍스트 필드'를 찾고 텍스트 입력 시뮬레이션
    final textFieldFinder = find.byKey(Key('텍스트 필드'));
    await tester.enterText(textFieldFinder, '테스트 입력');
    await tester.pump();

    // 입력된 텍스트가 올바른지 확인
    expect(find.text('테스트 입력'), findsOneWidget);
  });
}

위 코드에서는 tap, drag, enterText 메서드를 사용하여 각각의 상호작용을 시뮬레이션하고, 해당 동작이 올바르게 수행되었는지 확인합니다.

상호작용 테스트 시 주의사항

  • 적절한 키 사용: 각 위젯에 고유한 키를 부여하여 정확한 식별이 가능하도록 합니다.
  • 애니메이션 처리: 상호작용 후 pumpAndSettle()을 호출하여 모든 애니메이션이 완료될 때까지 기다립니다.
  • 테스트 환경 설정: 테스트 환경에서의 제약 사항을 고려하여 테스트를 설계합니다.

결론

Flutter에서 탭, 드래그, 텍스트 입력과 같은 사용자 상호작용을 테스트하는 것은 앱의 신뢰성과 사용자 경험을 향상시키는 데 필수적입니다. flutter_test 패키지와 WidgetTester의 다양한 메서드를 활용하여 이러한 상호작용을 철저히 검증하고, 앱의 품질을 높이세요.

 

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Add and remove a todo', (tester) async {
    // Build the widget.
    await tester.pumpWidget(const TodoList());

    // Enter 'hi' into the TextField.
    await tester.enterText(find.byType(TextField), 'hi');

    // Tap the add button.
    await tester.tap(find.byType(FloatingActionButton));

    // Rebuild the widget with the new item.
    await tester.pump();

    // Expect to find the item on screen.
    expect(find.text('hi'), findsOneWidget);

    // Swipe the item to dismiss it.
    await tester.drag(find.byType(Dismissible), const Offset(500, 0));

    // Build the widget until the dismiss animation ends.
    await tester.pumpAndSettle();

    // Ensure that the item is no longer on screen.
    expect(find.text('hi'), findsNothing);
  });
}

class TodoList extends StatefulWidget {
  const TodoList({super.key});

  @override
  State<TodoList> createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
  static const _appTitle = 'Todo List';
  final todos = <String>[];
  final controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: _appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(_appTitle),
        ),
        body: Column(
          children: [
            TextField(
              controller: controller,
            ),
            Expanded(
              child: ListView.builder(
                itemCount: todos.length,
                itemBuilder: (context, index) {
                  final todo = todos[index];

                  return Dismissible(
                    key: Key('$todo$index'),
                    onDismissed: (direction) => todos.removeAt(index),
                    background: Container(color: Colors.red),
                    child: ListTile(title: Text(todo)),
                  );
                },
              ),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              todos.add(controller.text);
              controller.clear();
            });
          },
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}