Flutter 앱을 테스트하는 방법

Flutter는 단일 코드베이스에서 멋진 네이티브 컴파일 모바일 애플리케이션, 웹 애플리케이션, 데스크톱 애플리케이션을 빌드할 수 있는 Google의 UI 도구 키트입니다.

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

학습할 내용

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

빌드할 프로그램

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

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

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

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

Android에서 실행 중인 앱의 GIF

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

이 주제를 처음 접하므로 개요를 파악하고 싶습니다. 이 주제에 관해 약간 알고 있지만 한 번 더 확인하고 싶습니다. 프로젝트에 사용할 코드 예를 찾고 있습니다. 특정 항목에 관한 설명을 찾고 있습니다.

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

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

  • 컴퓨터에 연결되어 있으며 개발자 모드로 설정된 실제 기기(Android 또는 iOS)
  • iOS 시뮬레이터 - Xcode 도구 설치 필요
  • Android Emulator - Android 스튜디오에서 설정 필요

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

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

b2f84ff91b0e1396.png첫 번째 Flutter 앱 시작하기의 안내에 따라 간단한 템플릿 Flutter 앱을 만듭니다. 프로젝트의 이름을 testing_app으로 지정합니다(myapp 대신). 이 시작 앱을 수정하여 완성된 앱을 만듭니다.

b2f84ff91b0e1396.pngIDE 또는 편집기에서 pubspec.yaml 파일을 엽니다. new로 표시된 다음 종속 항목을 추가한 후 파일을 저장합니다. (파일을 더 쉽게 읽을 수 있도록 주석을 삭제해도 됩니다.)

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.3
  provider: ^4.1.3            # new

dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test: ^1.0.1    # new
  test: ^1.14.4               # new
  1. IDE에서 프로젝트 상단의 Pub get 버튼을 클릭하거나 명령줄에서 flutter pub get을 실행합니다.

이 명령의 실행으로 인해 오류가 발생하면 공백(Tab 아님)을 사용한 dependencies 블록의 들여쓰기가 위에 나와 있는 것과 정확하게 동일한지 확인해야 합니다. YAML 파일은 공백에 민감합니다.

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

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

lib/main.dart의 내용 바꾸기

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

lib/main.dart

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

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

class TestingApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp(
        title: 'Testing Sample',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        routes: {
          HomePage.routeName: (context) => HomePage(),
          FavoritesPage.routeName: (context) => FavoritesPage(),
        },
        initialRoute: HomePage.routeName,
      ),
    );
  }
}

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

b2f84ff91b0e1396.pnglib 디렉터리에 screens라는 새 디렉터리를 만들고 새로 만든 이 디렉터리에 home.dart라는 새 파일을 만듭니다. lib/screens/home.dart에서 다음 코드를 추가합니다.

lib/screens/home.dart

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            style: TextButton.styleFrom(primary: Colors.white),
            onPressed: () {
              Navigator.pushNamed(context, FavoritesPage.routeName);
            },
            icon: Icon(Icons.favorite_border),
            label: 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,
  );

  @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)
              ? Icon(Icons.favorite)
              : 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: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

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

b2f84ff91b0e1396.pnglib/screens 디렉터리에 favorites.dart라는 또 다른 새 파일을 만듭니다. 이 파일에서 다음 코드를 추가합니다.

lib/screens/favorites.dart

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

class FavoritesPage extends StatelessWidget {
  static String routeName = '/favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: 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 {
  final int itemNo;

  const FavoriteItemTile(
    this.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: Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

마지막으로, lib/models/favorites.dart에서 Favorites 모델 만들기

b2f84ff91b0e1396.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();
  }
}

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

b2f84ff91b0e1396.png편집기에서 Run 아이콘 6869d41b089cc745.png을 클릭하여 앱을 실행합니다. 앱을 처음 실행하면 어느 정도 시간이 걸릴 수 있습니다. 이후 단계에서는 앱이 더 빠르게 실행됩니다. 앱 실행 화면은 다음 스크린샷과 같습니다.

be938199b599b605.png

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

이제 앱을 테스트할 준비가 되었습니다. 다음 단계부터는 앱을 테스트합니다.

먼저, favorites 모델을 단위 테스트합니다. 단위 테스트란 무엇인가요? 단위 테스트에서는 소프트웨어의 모든 개별 단위(흔히 함수)가 의도된 작업을 올바르게 실행하는지 확인합니다.

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

test/widget_test.dart 삭제

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

새 테스트 파일 만들기

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

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

test/models/favorites_test.dart

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

void main() {
  group('App Provider Tests', () {
    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을 사용합니다.

b2f84ff91b0e1396.png목록에서 항목을 삭제하는 것을 테스트합니다. 동일한 테스트 그룹에 다음 테스트를 복사하여 붙여넣습니다. 다음 코드를 테스트 파일에 추가합니다.

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

테스트 실행

b2f84ff91b0e1396.png앱이 에뮬레이터 또는 기기에서 실행 중인 경우 계속하기 전에 앱을 닫습니다.

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

$ flutter test test/models/favorites_test.dart

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

00:06 +2: All tests passed!

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

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

이 단계에서는 위젯 테스트를 실행합니다. 위젯 테스트는 개발자가 원하는 모든 개별 위젯을 테스트할 수 있는 Flutter의 고유한 테스트입니다. 이 단계에서는 화면(HomePageFavoritesPage)을 개별적으로 테스트합니다.

위젯 테스트에서는 test() 함수 대신 testWidget() 함수를 사용합니다. 또한 두 매개변수인 description,callback도 사용합니다. 그러나 여기서는 콜백이 WidgetTester를 인수로 사용합니다.

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

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

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

새 테스트 파일 만들기

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

b2f84ff91b0e1396.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: 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), Offset(0, -200), 3000);
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

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

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

b2f84ff91b0e1396.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), Offset(0, -200), 3000);
    await tester.pumpAndSettle();
    expect(find.text('Item 0'), findsNothing);
  });
});

테스트 실행

단위 테스트와 동일한 방식으로 위젯 테스트를 실행할 수 있습니다. 그런데 기기 또는 에뮬레이터를 사용하면 테스트 실행을 관찰할 수 있으며, 핫 리스타트를 사용할 수도 있습니다.

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

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

$ flutter run test/home_test.dart

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

Launching test/home_test.dart on Mi A3 in debug mode...
Running Gradle task 'assembleDebug'...
Running Gradle task 'assembleDebug'... Done                        62.7s
✓ Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app.apk...                 5.8s
Waiting for Mi A3 to report its views...                            16ms
I/flutter ( 1616): 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Mi A3...
I/flutter ( 1616): 00:02 +1: Home Page Widget Tests Testing Scrolling
Syncing files to device Mi A3...                                                 4,008ms (!)

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
An Observatory debugger and profiler on Mi A3 is available at:
http://127.0.0.1:40433/KOsGesHSxR8=/
I/flutter ( 1616): 00:00 +0: Home Page Widget Tests Testing if ListView shows up
I/flutter ( 1616): 00:02 +1: Home Page Widget Tests Testing Scrolling
I/flutter ( 1616): 00:09 +3: All tests passed!

다음으로, 테스트 파일을 변경하고 Shift + R 키를 입력하여 앱을 핫 리스타트하고 모든 테스트를 다시 실행합니다.

b2f84ff91b0e1396.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(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(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로 되돌아가는지 확인합니다.

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

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

b2f84ff91b0e1396.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';

Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: 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);
    });
  });
}

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

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

통합 테스트를 사용하면 앱의 개별 요소가 전체적으로 함께 작동하는 방식을 테스트할 수 있습니다. integration_test 패키지를 사용하여 Flutter에서 통합 테스트를 실행할 수 있습니다. 이 패키지는 Flutter의 Selenium WebDriver(일반 웹), Protractor(Angular), Espresso(Android) 또는 Earl Gray(iOS) 버전입니다. 이 패키지는 내부적으로 flutter_driver를 사용하여 기기에서 테스트를 구동합니다.

앱 계측

통합 테스트를 작성하려면 먼저, 앱을 계측해야 합니다. 앱 계측은 자동화된 테스트를 만들고 실행할 목적으로 드라이버가 GUI 및 함수에 액세스할 수 있도록 앱을 구성하는 것을 의미합니다. 통합 테스트는 integration_test라는 디렉터리에 배치됩니다. 이 단계에서는 통합 테스트를 위해 다음 파일을 추가합니다.

  • integration_test/driver.dart - 앱을 계측합니다.
  • integration_test/app_test.dart - 앱에서 실제 테스트를 실행합니다.

b2f84ff91b0e1396.png프로젝트의 루트 디렉터리에 integration_test라는 디렉터리를 만듭니다. 새로 만든 이 디렉터리에서 driver.dart 파일을 만들고 다음 코드를 추가합니다.

integration_test/driver.dart

import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();

이 코드는 통합 테스트 드라이버를 사용 설정한 후 테스트가 실행될 때까지 기다립니다. 테스트가 실행된 후 응답 데이터는 integration_response_data.json이라는 파일에 저장됩니다.

테스트 작성

b2f84ff91b0e1396.png새 파일을 만들어 이름을 app_test.dart로 지정합니다.

integration_test/app_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 Tests', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
        as IntegrationTestWidgetsFlutterBinding;

    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
  });
}

ensureInitialized() 함수는 통합 테스트 드라이버가 초기화되었는지 여부를 확인하고 필요한 경우 드라이버를 다시 초기화합니다. framePolicyLiveTestWidgetsFlutterBindingFramePolicy enum에서 fullyLive로 설정된 경우 애니메이션이 많은 상황을 테스트하는 데 가장 적합합니다.

다음으로, 앱의 스크롤 성능을 테스트하여 watchPerformance() 함수를 사용해 기록합니다.

b2f84ff91b0e1396.png방금 만든 테스트 그룹에 다음 코드를 붙여넣습니다.

integration_test/app_test.dart

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

  final listFinder = find.byType(ListView);

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

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

이 테스트는 항목 목록을 매우 빠르게 스크롤한 후 위로 끝까지 스크롤합니다. watchPerformance() 함수는 작업을 기록하고 타임라인 요약을 생성한 후 응답 데이터로서 driver.dart 파일의 테스트 드라이버에 다시 보냅니다.

다음으로, addremove 작업을 테스트합니다.

b2f84ff91b0e1396.png동일한 그룹에 다음 테스트를 붙여넣습니다.

integration_test/app_test.dart

testWidgets('Favorites operations test', (tester) async {
  await tester.pumpWidget(TestingApp());

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

  for (var icon in iconKeys) {
    await tester.tap(find.byKey(ValueKey(icon)));
    await tester.pumpAndSettle(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(Duration(seconds: 1));

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

테스트 실행

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

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

$ flutter drive --driver integration_test/driver.dart --target integration_test/app_test.dart --profile

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

Running "flutter pub get" in step_07...                            930ms
Running Gradle task 'assembleProfile'...
Running Gradle task 'assembleProfile'... Done                      31.3s
✓ Built build/app/outputs/flutter-apk/app-profile.apk (11.3MB).
Installing build/app/outputs/flutter-apk/app.apk...                277ms
VMServiceFlutterDriver: Connecting to Flutter application at http://127.0.0.1:62862/K6QKjUNab8c=/
VMServiceFlutterDriver: Isolate found with number: 1935648057883071
VMServiceFlutterDriver: Isolate is paused at start.
VMServiceFlutterDriver: Attempting to resume isolate
I/flutter (24385): 00:00 +0: Testing App Performance Tests Scrolling test
VMServiceFlutterDriver: Connected to Flutter application.
I/flutter (24385): 00:08 +1: Testing App Performance Tests Favorites operations
test
I/flutter (24385): 00:17 +2: Testing App Performance Tests (tearDownAll)
I/flutter (24385): 00:17 +3: All tests passed!
All tests passed.

테스트가 성공적으로 완료되면 프로젝트 루트의 build 디렉터리에 integration_response_data.json이라는 파일 하나가 있어야 합니다. 이 파일에는 테스트 실행 중에 테스트에서 다시 전송된 응답 데이터(이 경우 scrolling_summary)가 포함되어 있습니다. 텍스트 편집기로 파일을 열어 정보를 확인합니다. 고급 설정을 사용하면 테스트가 실행될 때마다 요약을 저장하고 결과 그래프를 생성할 수 있습니다.

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

Flutter 드라이버(통합) 테스트에 관한 자세한 내용은 다음을 참고하세요.

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

학습한 내용

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

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