본문 바로가기
App/Flutter

Flutter 테스트 유형 및 방법

by Day0404 2024. 4. 4.
728x90
반응형

오늘은 Flutter의 테스트 종류와 그 방법에 대해 글을 쓰고자 합니다.

 

Flutter에는 3가지 종류의 테스트 방법이 있습니다.

1. 단위 테스트 (Unit tests)

단위 테스트는 메서드나 클래스의 동작을 확인합니다. 특정 코드 단위를 분리하여 테스트하기 때문에 다른 부분의 영향을 받지 않고 테스트할 수 있으며 이를 통해 메서드나 클래스가 잘 작동하는지 확인할 수 있습니다. 단위 테스트의 목적은 특정 코드 단위의 동작을 확인하여 코드의 품질을 개선하고 버그를 방지하는 데 있습니다.

 

2. 위젯 테스트 (Widget tests)

위젯 테스트는 UI 요소의 개별 동작을 테스트합니다. 버튼이 제대로 클릭이 되는지 텍스트 필드에 입력이 제대로 반영이 되는지 등을 확인합니다. 단위 테스트가 개별 함수, 메서드, 클래스의 기능을 테스트하며 비즈니스 로직이나 데이터 처리와 같은 특정 기능을 대상으로 테스트 하는 반면에 위젯 테스트는 UI 요소의 동작에 집중합니다. 주로 위젯의 렌더링, 상태 변화, 사용자 입력 등을 테스트합니다.

 

3. 통합 테스트 (Integration tests)

통합 테스트는 애플리케이션의 여러 부분을 통합하여 전체 애플리케이션의 동작을 테스트합니다. 여러 위젯이나 화면 간의 상호 작용 및 전환, 데이터 흐름 등을 테스트합니다. 단위 테스트와 위젯 테스트와는 다르게 통합 테스트는 실제 디바이스나 시뮬레이터, 에뮬레이터를 실행하여 앱의 실제 환경에서 테스트를 진행합니다. 이후 작성한 테스트 시나리오대로 테스트를 진행하게 됩니다.

 

 

간단한 카운터 애플리케이션 예제와 함께 테스트 방법에 대해 알아보겠습니다.

 

main.dart

import 'package:flutter/material.dart';

import 'counter.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: CounterApp(),
    );
  }
}

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

  @override
  State<CounterApp> createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  final Counter _counter = Counter();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter App'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Count:',
              style: TextStyle(fontSize: 24),
            ),
            Text(
              '${_counter.value}',
              style: const TextStyle(fontSize: 36),
            ),
          ],
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: () {
              setState(() {
                _counter.increment();
              });
            },
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 16),
          FloatingActionButton(
            onPressed: () {
              setState(() {
                _counter.decrement();
              });
            },
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

 

counter.dart

class Counter {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
  }

  void decrement() {
    _value--;
  }
}

 

위 예제는 간단한 카운터 예제입니다. 이제 이 예제를 바탕으로 단위 테스트, 위젯 테스트, 통합 테스트를 진행해 보겠습니다.

 

먼저 테스트 코드를 작성하기에 앞서 pubspec.yml에 아래와 같이 flutter_test 패키지를 추가해야 합니다. 이 패키지는 프로젝트 생성 시 자동으로 추가가 되기 때문에 따로 추가할 필요는 없으며 통합 테스트를 위해 integration_test 정도를 미리 추가하도록 합니다.

 

 

 

테스트 디렉토리 구조

 

Flutter에서 테스트를 진행할 때 파일 하나에 모든 테스트를 진행하지 않기 때문에 각 테스트 유형별로 디렉토리 구조를 나눕니다. 이후 프로젝트가 커짐에 따라 디렉토리 구조는 더 세밀하게 나눠질 겁니다. 그리고 Flutter 테스트 코드를 작성할 때 파일명 뒤에는 test를 붙여주도록 합니다.

단위 테스트 (Unit tests)

test/unit/unit_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_example/counter.dart';

void main() {
  group('Counter', () {
    test('starts with 0', () {
      final counter = Counter();
      expect(counter.value, 0);
    });

    test('increments value', () {
      final counter = Counter();
      counter.increment();
      expect(counter.value, 1);
    });

    test('decrements value', () {
      final counter = Counter();
      counter.decrement();
      expect(counter.value, -1);
    });
  });
}

 

group()

테스트를 그룹화하는 데 사용됩니다. 이 함수를 통해 특정 기능 또는 클래스에 대한 테스트를 그룹화할 수 있습니다. 위 코드에서는 Counter라는 그룹으로 만들었습니다.

 

test()

개별 테스트를 정의합니다. 여기서는 특정 동작을 검증하고, 해당 동작이 개발자의 예상대로 작동하는지 확인합니다. 위 코드에서는 'starts with 0', 'increments value', 'decrements value' 3가지 테스트를 정의했습니다.

starts with 0은 카운터가 0에서 시작하는지 확인합니다.

increments value는 increment 메서드가 호출될 때 카운터 값이 +1씩 증가하는지 확인합니다.

decrements value는 decrement 메서드가 호출될 때 카운터 값이 -1씩 감소하는지 확인합니다.

 

expect()

이 함수는 특정 조건이 충족되는지 확인할 때 사용됩니다. 영어 뜻 그대로 예상 값이 실제 값과 일치하는지 확인하여 예상에 일치하면 테스트를 통과하고 일치하지 않으면 실패하게 됩니다.

 

위 단위 테스트 예제 코드를 실행시키면 아래와 같이 정상적으로 작동하는 것을 확인할 수 있습니다.

 

만약 starts with 0 테스트에 expect 값을 1로 고쳐서 실행시키면 아래와 같이 예상은 1로 했지만 실제 값은 0이라는 테스트 실패 결과가 나옵니다.

 

위젯 테스트 (Widget tests)

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

void main() {
  testWidgets('Counter App UI Test', (WidgetTester tester) async {
    // 빌드하고 화면을 렌더링합니다.
    await tester.pumpWidget(const MyApp());

    // "Count:" 텍스트가 화면에 표시되는지 확인합니다.
    expect(find.text('Count:'), findsOneWidget);

    // 카운터 초기 값이 0인지 확인합니다.
    expect(find.text('0'), findsOneWidget);

    // "+" FloatingActionButton이 화면에 표시되는지 확인합니다.
    expect(find.byIcon(Icons.add), findsOneWidget);

    // "-" FloatingActionButton이 화면에 표시되는지 확인합니다.
    expect(find.byIcon(Icons.remove), findsOneWidget);
  });

  testWidgets('Counter App Increment Test', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());

    // "+" 버튼을 탭합니다.
    await tester.tap(find.byIcon(Icons.add));

    // 화면을 다시 그리고, 카운터 값이 1인지 확인합니다.
    await tester.pump();

    expect(find.text('1'), findsOneWidget);
  });

  testWidgets('Counter App Decrement Test', (WidgetTester tester) async {
    await tester.pumpWidget(const MyApp());

    // "-" 버튼을 탭합니다.
    await tester.tap(find.byIcon(Icons.remove));

    // 화면을 다시 그리고, 카운터 값이 -1인지 확인합니다.
    await tester.pump();

    expect(find.text('-1'), findsOneWidget);
  });
}

 

testWidgets()

이 함수는 위젯 테스트를 정의하는 데 사용됩니다. 각 testWidgets() 함수는 위젯 트리를 빌드하고 해당 위젯이 예상대로 동작하는지 확인합니다.

 

WidgetTester()

이 클래스는 위젯 테스트를 실행하는 데 사용되는 객체입니다. 위젯을 빌드하고 화면을 다시 그리는 등의 작업을 수행할 수 있습니다.

 

pumpWidget()

WidgetTester의 메서드로 위젯을 빌드하고 화면에 렌더링 하는 역할을 합니다.

 

위 예제 코드에는 3가지 testWidgets이 있습니다.

Counter App UI Test : 카운터 앱의 UI를 테스트합니다. 이 테스트에선 'Count' 텍스트가 화면에 표시되는지, 초기 카운터 값이 0인지, 각 '+', '-' FloatingActionButton이 화면에 표시되는지 테스트합니다.

Counter App Increment Test : 카운터 앱의 증가 기능을 테스트합니다. 위 예제에서는 '+' 버튼을 클릭하고 카운터 값이 1 증가하는지 확인합니다.

Counter App Decrement Test : 카운터 앱의 감소 기능을 테스트합니다. Counter App Increment Test 테스트와 반대로 버튼 클릭 후 값이 1 감소하는지 확인합니다.

 

위 위젯 테스트를 실행시키면 아래와 같이 정상적으로 테스트를 통과하게 됩니다.

 

통합 테스트 (Integration tests)

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test_example/main.dart' as app;

void main() {
  group('Counter App', () {
    IntegrationTestWidgetsFlutterBinding.ensureInitialized();

    testWidgets('Counter increment test', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 앱이 시작되었는지 확인
      expect(find.text('Count:'), findsOneWidget);

      // 초기 카운터 값이 0인지 확인
      expect(find.text('0'), findsOneWidget);

      // "+" 버튼을 탭하고 화면 갱신 대기
      await tester.tap(find.byIcon(Icons.add));
      await tester.pumpAndSettle();

      // 카운터 값이 1인지 확인
      expect(find.text('1'), findsOneWidget);
    });

    testWidgets('Counter decrement test', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      // 초기 카운터 값이 0인지 확인
      expect(find.text('0'), findsOneWidget);

      // "-" 버튼을 탭하고 화면 갱신 대기
      await tester.tap(find.byIcon(Icons.remove));
      await tester.pumpAndSettle();

      // 카운터 값이 -1인지 확인
      expect(find.text('-1'), findsOneWidget);
    });
  });
}

 

저는 이번 예제에서 통합 테스트를 사용할 때 test_driver를 사용하는 방식이 아닌 integration_test 패키지를 사용하여 진행했습니다. test_driver의 경우 앱의 UI와 상호 작용을 테스트하고 실제 환경에서 앱을 실행하여 테스트할 때 사용합니다. 예를 들어 사용자가 앱의 버튼을 클릭했을 때 앱이 개발자의 예상대로 동작하는지 확인할 때 사용할 수 있습니다.

integration_test의 경우 앱의 전반적인 흐름, 비즈니스 로직, 여러 화면 간의 통합을 테스트할 때 사용합니다. 예를 들어 여러 페이지 간의 이동이나 데이터 흐름을 테스트하고 싶을 때 사용할 수 있습니다. 또한 integration_test를 사용하면 에뮬레이터나 실제 기기를 연결하지 않고도 테스트를 진행할 수 있습니다.

 

IntegrationTestWidgetsFlutterBinding.ensureInitialized()

이 메서드는 통합 테스트를 위한 바인딩을 초기화합니다. 실제 기기나 에뮬레이터를 사용하지 않고도 통합 테스트를 수행할 수 있게 합니다.

 

통합 테스트 역시 정상적으로 테스트를 통과하는 것을 확인할 수 있습니다.

 

테스트 코드를 작성하면서 앱을 만들게 되면 앱의 안정성이 향상됩니다. 더욱 견고한 코드를 작성할 수 있고 버그를 발견, 수정할 수 있고 새로운 기능이 추가될 때 기존 기능을 손상시키지 않을 수 있습니다. 여기에 더 나아가 테스트를 CI/CD 시스템과 함께 자동화를 진행한다면 코드를 변경할 때마다 자동으로 테스트가 실행되고, 테스트 결과에 따라 자동으로 앱을 빌드하고 배포할 수 있습니다.

 

반응형

댓글