Flutter 앱을 테스트하는 방법

1. 소개

Flutter는 하나의 코드베이스를 사용해 모바일, 웹, 데스크톱을 대상으로 아름다운 네이티브 컴파일 애플리케이션을 빌드하기 위한 Google의 UI 툴킷입니다.

이 Codelab에서는 간단한 Flutter 앱을 빌드하고 테스트합니다. 앱은 상태를 관리하는 데 Provider 패키지를 사용합니다.

학습할 내용

  • 위젯 테스트 프레임워크를 사용하여 위젯 테스트를 만드는 방법
  • integration_test 라이브러리를 사용하여 앱의 UI와 성능을 테스트하는 통합 테스트를 만드는 방법
  • 단위 테스트를 활용하여 데이터 클래스(Provider)를 테스트하는 방법

빌드할 프로그램

이 Codelab에서는 먼저, 항목 목록이 있는 간단한 애플리케이션을 빌드합니다. 테스트에 바로 들어갈 수 있도록 소스 코드를 제공합니다. 앱은 다음과 같은 작업을 지원합니다.

  • 즐겨찾기에 항목 추가
  • 즐겨찾기 목록 보기
  • 즐겨찾기 목록에서 항목 삭제

앱이 완성되면 다음과 같은 테스트를 작성합니다.

  • 추가 및 삭제 작업의 유효성을 검사하는 단위 테스트
  • 홈페이지 및 즐겨찾기 페이지의 위젯 테스트
  • 통합 테스트를 사용하여 앱 전체의 UI 및 성능 테스트

Android에서 실행 중인 앱의 GIF

이 Codelab에서 학습하고 싶은 내용은 무엇인가요?

주제를 처음 접하기 때문에 간단하게 내용을 파악하고 싶습니다. 이 주제에 관해 약간 알고 있지만 한 번 더 확인하고 싶습니다. 프로젝트에 사용할 코드 예를 찾고 있습니다. 구체적인 항목에 관한 설명을 찾고 있습니다.

2. Flutter 개발 환경 설정

이 실습을 완료하려면 Flutter SDK편집기라는 두 가지 소프트웨어가 필요합니다.

다음 기기 중 하나를 사용하여 이 Codelab을 실행할 수 있습니다.

  • 컴퓨터에 연결되어 있고 개발자 모드로 설정된 실제 Android 또는 iOS 기기
  • iOS 시뮬레이터(Xcode 도구 설치 필요)
  • Android Emulator(Android 스튜디오 설정 필요)
  • 브라우저(디버깅 시 Chrome 필요)
  • Windows, Linux 또는 macOS 데스크톱 애플리케이션. 앱을 배포할 플랫폼에서 개발해야 합니다. 따라서 Windows 데스크톱 앱을 개발하려면 적합한 빌드 체인에 액세스할 수 있도록 Windows에서 개발해야 합니다. 자세한 운영체제별 요구사항은 docs.flutter.dev/desktop을 참고하세요.

3. 시작하기

새 Flutter 앱 만들기 및 종속 항목 업데이트

이 Codelab에서는 Flutter 모바일 앱을 테스트하는 데 중점을 둡니다. 따라서 복사하여 붙여넣은 소스 파일을 사용해서 테스트할 앱을 빠르게 만듭니다. 그런 다음, Codelab의 나머지 부분에서는 다양한 종류의 테스트를 알아보는 데 중점을 둡니다.

a3c16fc17be25f6c.png첫 번째 Flutter 앱 시작하기의 안내를 사용하거나 명령줄에 다음과 같이 입력하여 간단한 템플릿 형식의 Flutter 앱을 만듭니다.

$ flutter create testing_app

a3c16fc17be25f6c.png명령줄에 pub 종속 항목을 추가합니다. 상태 관리를 쉽게 하기 위해 provider를 추가합니다.

$ cd testing_app
$ flutter pub add provider
Resolving dependencies...
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ nested 1.0.0
  path 1.8.2 (1.8.3 available)
+ provider 6.0.5
  test_api 0.4.16 (0.4.18 available)
Changed 2 dependencies!

기기와 에뮬레이터에서 Flutter 코드를 자율적으로 구동하여 테스트하기 위해 integration_test를 추가합니다.

$ flutter pub add --dev --sdk=flutter integration_test
Resolving dependencies...
+ archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ crypto 3.0.2
+ file 6.1.4
+ flutter_driver 0.0.0 from sdk flutter
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ integration_test 0.0.0 from sdk flutter
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
+ platform 3.1.0
+ process 4.2.4
+ sync_http 0.3.1
  test_api 0.4.16 (0.4.18 available)
+ typed_data 1.3.1
+ vm_service 9.4.0 (11.0.1 available)
+ webdriver 3.0.1 (3.0.2 available)
Changed 12 dependencies!

실제 기기와 에뮬레이터에서 실행되는 Flutter 애플리케이션을 테스트하는 고급 API를 위해 flutter_driver를 추가합니다.

$ flutter pub add --dev --sdk=flutter flutter_driver
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test_api 0.4.16 (0.4.18 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!

일반적인 테스트 도구를 위해 test를 추가합니다.

$ flutter pub add --dev test
Resolving dependencies...
+ _fe_analyzer_shared 52.0.0
+ analyzer 5.4.0
  archive 3.3.2 (3.3.6 available)
+ args 2.3.2
  collection 1.17.0 (1.17.1 available)
+ convert 3.1.1
+ coverage 1.6.3
+ frontend_server_client 3.2.0
+ glob 2.1.1
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ io 1.0.4
  js 0.6.5 (0.6.7 available)
+ logging 1.1.1
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ mime 1.0.4
+ node_preamble 2.0.1
+ package_config 2.1.0
  path 1.8.2 (1.8.3 available)
+ pool 1.5.1
+ pub_semver 2.1.3
+ shelf 1.4.0
+ shelf_packages_handler 3.0.1
+ shelf_static 1.1.1
+ shelf_web_socket 1.0.3
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.11
+ test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
+ test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
+ watcher 1.0.2
+ web_socket_channel 2.3.0
  webdriver 3.0.1 (3.0.2 available)
+ webkit_inspection_protocol 1.2.0
+ yaml 3.1.1
Changed 28 dependencies!

앱 탐색을 처리하기 위해 go_router를 추가합니다.

$ flutter pub add go_router
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 6.0.4
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Changed 2 dependencies!

다음 종속 항목이 pubspec.yaml에 추가되어 있어야 합니다.

dependencies 아래는 다음과 같습니다.

dependencies:
  provider: ^6.0.5
  go_router: ^6.0.4

dev_dependencies 아래는 다음과 같습니다.

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  test: ^1.22.0

a3c16fc17be25f6c.png선택한 코드 편집기에서 프로젝트를 열고 앱을 실행합니다. 또는 다음과 같이 명령줄에서 앱을 실행합니다.

$ flutter run

4. 앱 빌드

다음으로, 앱을 테스트할 수 있도록 빌드합니다. 앱에는 다음과 같은 파일이 포함됩니다.

  • lib/models/favorites.dart - 즐겨찾기 목록의 모델 클래스를 만듭니다.
  • lib/screens/favorites.dart - 즐겨찾기 목록의 레이아웃을 만듭니다.
  • lib/screens/home.dart - 항목 목록을 만듭니다.
  • lib/main.dart - 앱이 시작되는 기본 파일입니다.

첫째, lib/models/favorites.dartFavorites 모델 만들기

a3c16fc17be25f6c.pnglib 디렉터리에 models라는 새로운 디렉터리를 만든 다음 favorites.dart라는 새 파일을 만듭니다. 이 파일에 다음 코드를 추가합니다.

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

lib/screens/favorites.dart에 즐겨찾기 페이지 추가하기

a3c16fc17be25f6c.pnglib 디렉터리에 screens라는 새로운 디렉터리를 만들고 그 디렉터리에 favorites.dart라는 새 파일을 만듭니다. 이 파일에 다음 코드를 추가합니다.

lib/screens/favorites.dart

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

import '../models/favorites.dart';

class FavoritesPage extends StatelessWidget {
  const FavoritesPage({super.key});

  static String routeName = 'favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  const FavoriteItemTile(this.itemNo, {super.key});

  final int itemNo;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: const Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

lib/screens/home.dart에 홈페이지 추가하기

a3c16fc17be25f6c.pnglib/screens 디렉터리에 home.dart라는 또 다른 파일을 새로 만듭니다. lib/screens/home.dart에 다음 코드를 추가합니다.

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/favorites.dart';
import 'favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () {
              context.go('/${FavoritesPage.routeName}');
            },
            icon: const Icon(Icons.favorite_border),
            label: const Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(this.itemNo, {super.key});

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? const Icon(Icons.favorite)
              : const Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: const Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

lib/main.dart의 내용 바꾸기

a3c16fc17be25f6c.pnglib/main.dart의 내용을 다음 코드로 바꿉니다.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'models/favorites.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';

void main() {
  runApp(const TestingApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: HomePage.routeName,
      builder: (context, state) {
        return const HomePage();
      },
      routes: [
        GoRoute(
          path: FavoritesPage.routeName,
          builder: (context, state) {
            return const FavoritesPage();
          },
        ),
      ],
    ),
  ],
);

class TestingApp extends StatelessWidget {
  const TestingApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp.router(
        title: 'Testing Sample',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

이제 앱이 완성되었지만 테스트는 하지 않았습니다.

a3c16fc17be25f6c.png앱을 실행합니다. 앱을 실행하면 다음 스크린샷과 같이 표시됩니다.

b74f843e42a28b0f.png

앱에 항목 목록이 표시됩니다. 아무 행에서나 하트 모양 아이콘을 탭하여 하트를 채우고 항목을 즐겨찾기 목록에 추가합니다. AppBarFavorites 버튼을 탭하면 즐겨찾기 목록이 포함된 두 번째 화면으로 이동합니다.

이제 앱을 테스트할 준비가 되었습니다. 다음 단계에서는 앱 테스트를 시작합니다.

5. Provider 단위 테스트

먼저, favorites 모델에 대한 단위 테스트를 시작해 보겠습니다. 단위 테스트란 무엇인가요? 단위 테스트는 소프트웨어의 모든 개별 단위(함수, 객체 또는 위젯)가 의도한 동작을 올바르게 실행하는지 확인합니다.

Flutter 앱의 모든 테스트 파일은 test 디렉터리에 있습니다(통합 테스트 제외).

test/widget_test.dart 삭제

a3c16fc17be25f6c.png테스트를 시작하기 전에 widget_test.dart 파일을 삭제합니다. 자체 테스트 파일을 추가하겠습니다.

새 테스트 파일 만들기

먼저, Favorites 모델의 add() 메서드를 테스트하여 새 항목이 목록에 추가되었는지와 목록에 변경사항이 반영되었는지 확인합니다. 규칙에 따라 test 디렉터리의 디렉터리 구조는 lib 디렉터리의 구조를 모방하고, Dart 파일은 같은 이름에 _test를 추가합니다.

a3c16fc17be25f6c.pngtest 디렉터리에 models 디렉터리를 만듭니다. 이 새 디렉터리에 다음과 같은 내용으로 favorites_test.dart 파일을 만듭니다.

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });
  });
}

Flutter 테스트 프레임워크를 사용하면 그룹 내에서 서로 관련된 유사한 테스트를 결합할 수 있습니다. lib 디렉터리에 있는 상응하는 파일의 다양한 부분을 테스트하기 위한 단일 테스트 파일에는 여러 그룹이 있을 수 있습니다.

test() 메서드는 위치가 지정된 2개의 매개변수를 사용합니다. 하나는 테스트의 description이며, 다른 하나는 실제로 테스트를 작성하는 callback입니다.

a3c16fc17be25f6c.png목록에서 항목을 삭제하는 것을 테스트합니다. 동일한 Testing App Provider 그룹에 다음 테스트를 삽입합니다.

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

테스트 실행

a3c16fc17be25f6c.png명령줄에서 프로젝트의 루트 디렉터리로 이동하여 다음 명령어를 입력합니다.

$ flutter test test/models/favorites_test.dart

모든 것이 제대로 작동한다면 다음과 유사한 메시지가 표시됩니다.

00:06 +2: All tests passed!

전체 테스트 파일은 test/models/favorites_test.dart에서 확인할 수 있습니다.

단위 테스트에 관한 자세한 내용은 단위 테스트 소개를 참고하세요.

6. 위젯 테스트

이 단계에서는 테스트 위젯에 코드를 추가합니다. 위젯 테스트는 Flutter의 고유한 테스트로, 격리된 방식으로 각 위젯을 테스트할 수 있습니다. 이 단계에서는 HomePage 화면과 FavoritesPage 화면을 개별적으로 테스트합니다.

위젯 테스트는 test() 함수 대신 testWidget() 함수를 사용합니다. test() 함수와 마찬가지로 testWidget() 함수는 두 개의 매개변수, description,callback을 사용하며, 콜백은 WidgetTester를 인수로 사용합니다.

위젯 테스트는 TestFlutterWidgetsBinding을 사용합니다. 이 클래스는 실행 중인 앱에서 사용하지만, 앱 내에서 실행하지는 않는 리소스와 동일한 리소스를 위젯에 제공합니다. 예를 들어, 화면 크기 정보, 애니메이션 예약 기능 등입니다. 대신, 위젯을 인스턴스화하는 데는 가상 환경을 사용하고 실행하면서 결과를 테스트합니다. 여기서 pumpWidget은 애플리케이션에서와 같이 특정 위젯을 마운트하고 측정하도록 프레임워크에 지시함으로써 프로세스를 시작합니다.

위젯 테스트 프레임워크는 위젯을 찾는 파인더(예: text(), byType(), byIcon().)를 제공합니다. 또한, 결과를 확인하기 위해 매처도 제공합니다.

먼저, HomePage 위젯을 테스트합니다.

새 테스트 파일 만들기

첫 번째 테스트는 HomePage 스크롤 기능이 제대로 작동하는지 확인합니다.

a3c16fc17be25f6c.pngtest 디렉터리에 새 파일을 만들어 이름을 home_test.dart로 지정합니다. 새로 만든 파일에서 다음 코드를 추가합니다.

test/home_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: const MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

createHomeScreen() 함수를 사용하여 ChangeNotifierProvider에 래핑된 MaterialApp에서 테스트할 위젯을 로드하는 앱을 만듭니다. 위젯 트리에서 HomePage 위젯의 상위에 이러한 두 위젯이 모두 있어야 합니다. 그래야 HomePage 위젯이 두 위젯에서 상속받고 두 위젯이 제공하는 데이터에 액세스할 수 있습니다. 이 함수를 pumpWidget() 함수에 매개변수로 전달합니다.

그런 다음, 프레임워크가 화면에 렌더링된 ListView를 찾을 수 있는지 테스트합니다.

a3c16fc17be25f6c.png다음 코드 스니펫을 home_test.dart에 추가합니다.

test/home_test.dart

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });
  // END OF NEW CONTENT

    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
});

테스트 실행

먼저, 단위 테스트를 실행한 것과 동일한 방식으로 명령어를 사용하여 테스트를 실행합니다.

$ flutter test test/home_test.dart

테스트는 신속하게 실행되어야 하며 다음과 같은 메시지가 표시됩니다.

00:02 +2: All tests passed!

또한, 기기나 에뮬레이터를 사용하여 위젯 테스트를 실행할 수 있습니다. 이렇게 하면 테스트가 실행되는 것을 관찰할 수 있습니다. 핫 리스타트 기능도 사용할 수 있습니다.

a3c16fc17be25f6c.png기기를 연결하거나 에뮬레이터를 시작합니다. 데스크톱 애플리케이션으로 테스트를 실행할 수도 있습니다.

a3c16fc17be25f6c.png명령줄에서 프로젝트의 루트 디렉터리로 이동하여 다음 명령어를 입력합니다.

$ flutter run test/home_test.dart

테스트를 실행할 기기를 선택해야 할 수도 있습니다. 그런 경우에는 다음 안내에 따라 기기를 선택합니다.

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"):

모든 것이 제대로 작동한다면 다음과 비슷한 출력이 표시됩니다.

Launching test/home_test.dart on Linux in debug mode...
Building Linux application...
flutter: 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Linux...                                    62ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:00 +1: Home Page Widget Tests Testing Scrolling
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:02 +2: All tests passed!

다음으로, 테스트 파일을 변경하고 Shift + R 키를 눌러 앱을 핫 리스타트하고 모든 테스트를 다시 실행합니다. 애플리케이션을 중지하면 안 됩니다.

a3c16fc17be25f6c.pngHomePage 위젯을 테스트하는 그룹에 테스트를 더 추가합니다. 다음 테스트를 파일에 복사합니다.

test/home_test.dart

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

이 테스트는 IconButton을 탭하면 Icons.favorite_border(비어 있는 하트)에서 Icons.favorite(채워진 하트)으로 변경되고 다시 탭하면 Icons.favorite_border로 되돌아가는지 확인합니다.

a3c16fc17be25f6c.pngShift + R 키를 입력합니다. 그러면 앱이 핫 리스타트되고 모든 테스트가 다시 실행됩니다.

전체 테스트 파일은 test/home_test.dart.에서 확인할 수 있습니다.

a3c16fc17be25f6c.png동일한 프로세스를 사용하여 다음 코드로 FavoritesPage를 테스트합니다. 동일한 단계를 따르고 실행합니다.

test/favorites_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

이 테스트는 닫기(삭제) 버튼을 누를 때 항목이 사라지는지 확인합니다.

위젯 테스트에 관한 자세한 내용은 다음을 참고하세요.

7. 통합 테스트로 앱 UI 테스트

통합 테스트는 앱의 개별 요소가 전체적으로 연동되는 방식을 테스트하는 데 사용됩니다. integration_test 라이브러리는 Flutter로 통합 테스트를 실행하는 데 사용됩니다. 이 라이브러리는 Flutter의 Selenium WebDriver, Protractor, Espresso, Earl Gray 버전입니다. 이 패키지는 내부적으로 flutter_driver를 사용하여 기기에서 테스트를 구동합니다.

Flutter로 통합 테스트를 작성하는 것은 통합 테스트가 대상 기기라고 하는 휴대기기, 브라우저 또는 데스크톱 애플리케이션에서 실행된다는 점을 제외하고 위젯 테스트를 작성하는 것과 비슷합니다.

테스트 작성

a3c16fc17be25f6c.png프로젝트 루트 디렉터리에 integration_test라는 디렉터리를 만들고 그 디렉터리에 app_test.dart라는 새 파일을 만듭니다.

integration_test/app_test.dart

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

void main() {
  group('Testing App', () {
    testWidgets('Favorites operations test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final iconKeys = [
        'icon_0',
        'icon_1',
        'icon_2',
      ];

      for (var icon in iconKeys) {
        await tester.tap(find.byKey(ValueKey(icon)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Added to favorites.'), findsOneWidget);
      }

      await tester.tap(find.text('Favorites'));
      await tester.pumpAndSettle();

      final removeIconKeys = [
        'remove_icon_0',
        'remove_icon_1',
        'remove_icon_2',
      ];

      for (final iconKey in removeIconKeys) {
        await tester.tap(find.byKey(ValueKey(iconKey)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Removed from favorites.'), findsOneWidget);
      }
    });
  });
}

테스트 실행

a3c16fc17be25f6c.png기기를 연결하거나 에뮬레이터를 시작합니다. 데스크톱 애플리케이션으로 테스트를 실행할 수도 있습니다.

a3c16fc17be25f6c.png명령줄에서 프로젝트의 루트 디렉터리로 이동하여 다음 명령어를 입력합니다.

$ flutter test integration_test/app_test.dart

모든 것이 제대로 작동한다면 다음과 비슷한 출력이 표시됩니다.

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
00:00 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                B00:08 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart
00:26 +1: All tests passed!

8. Flutter Driver를 사용하여 앱 성능 테스트

성능 테스트 작성

integration_test 폴더에 다음 내용을 포함하는 perf_test.dart라는 새로운 테스트 파일을 만듭니다.

integration_test/perf_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App Performance', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

    testWidgets('Scrolling test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final listFinder = find.byType(ListView);

      await binding.traceAction(() async {
        await tester.fling(listFinder, const Offset(0, -500), 10000);
        await tester.pumpAndSettle();

        await tester.fling(listFinder, const Offset(0, 500), 10000);
        await tester.pumpAndSettle();
      }, reportKey: 'scrolling_summary');
    });
  });
}

ensureInitialized() 함수는 통합 테스트 드라이버가 초기화되고 필요시 드라이버를 다시 초기화하는지 확인합니다. framePolicyfullyLive로 설정하면 애니메이션 코드를 테스트하는 데 좋습니다.

이 테스트는 항목 목록을 매우 빠르게 스크롤한 후 위로 끝까지 스크롤합니다. traceAction() 함수는 작업을 기록하고 타임라인 요약을 생성합니다.

성능 결과 캡처

결과를 캡처하려면 perf_driver.dart라는 파일이 포함된 test_driver 폴더를 만들고 다음 코드를 파일에 추가합니다.

test_driver/perf_driver.dart

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
            data['scrolling_summary'] as Map<String, dynamic>);

        final summary = driver.TimelineSummary.summarize(timeline);

        await summary.writeTimelineToFile(
          'scrolling_summary',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

테스트 실행

a3c16fc17be25f6c.png기기를 연결하거나 에뮬레이터를 시작합니다.

a3c16fc17be25f6c.png명령줄에서 프로젝트의 루트 디렉터리로 이동하여 다음 명령어를 입력합니다.

$ flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/perf_test.dart \
  --profile \
  --no-dds

모든 것이 제대로 작동한다면 다음과 비슷한 출력이 표시됩니다.

Running "flutter pub get" in testing_app...
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!
Running Gradle task 'assembleProfile'...                         1,379ms
✓  Built build/app/outputs/flutter-apk/app-profile.apk (14.9MB).
Installing build/app/outputs/flutter-apk/app-profile.apk...        222ms
I/flutter ( 6125): 00:04 +1: Testing App Performance (tearDownAll)
I/flutter ( 6125): 00:04 +2: All tests passed!
All tests passed.

테스트가 성공적으로 완료되면 프로젝트 루트에 있는 빌드 디렉터리에 두 개의 파일이 생깁니다.

  1. scrolling_summary.timeline_summary.json: 요약을 포함합니다. 텍스트 편집기로 파일을 열어 안에 포함된 정보를 검토합니다.
  2. scrolling_summary.timeline.json: 전체 타임라인 데이터를 포함합니다.

통합 테스트에 관한 더 자세한 세부정보는 다음을 방문하세요.

9. 축하합니다.

Codelab을 완료하고 Flutter 앱을 테스트하는 다양한 방법을 알아보았습니다.

학습한 내용

  • 단위 테스트를 활용하여 Provider를 테스트하는 방법
  • 위젯 테스트 프레임워크를 사용하여 위젯을 테스트하는 방법
  • 통합 테스트를 사용하여 앱의 UI를 테스트하는 방법
  • 통합 테스트를 사용하여 앱 성능을 테스트하는 방법

Flutter에서 테스트하는 방법에 관해 자세히 알아보려면 다음을 참고하세요.