티스토리 뷰

Flutter 개발에서 단위 테스트는 앱의 기능을 검증하는 필수 과정입니다. 하지만 때때로 특정 함수나 외부 의존성을 테스트할 수 없을 때 Mocking 기법이 필요합니다. 이번 포스트에서는 Flutter에서 Mocking을 활용하는 방법을 이해하기 쉽게 설명하고, 테스트 코드 작성 시 유용한 팁과 사례를 공유하겠습니다.

참고. Mock dependencies using Mockito

Mocking이란 무엇인가?

Mocking은 단위 테스트를 실행할 때 외부 의존성이나 복잡한 객체를 가짜(Mock) 객체로 대체하는 기법입니다. 예를 들어 네트워크 요청, 데이터베이스 호출, API 응답 등 외부 요소를 테스트할 수 없는 상황에서 Mock 객체를 사용하여 코드의 동작을 검증할 수 있습니다.

Mocking의 장점

  1. 의존성 제거: 네트워크, 데이터베이스 등 외부 환경에 의존하지 않습니다.
  2. 테스트 속도 향상: 가벼운 Mock 객체를 사용하므로 테스트가 더 빠르게 실행됩니다.
  3. 유연한 검증: 특정 함수의 반환값을 설정하여 다양한 시나리오를 테스트할 수 있습니다.

Flutter에서 Mocking 설정하기

Flutter에서는 Mocking을 위해 주로 mockito 패키지를 사용합니다. mockito는 간단하고 강력한 Mock 객체 생성을 지원합니다.

1. mockito 패키지 설치

먼저 pubspec.yaml 파일에 mockito 패키지를 추가합니다:

dev_dependencies:
  mockito: ^5.0.0
  flutter_test:
    sdk: flutter

이후 패키지를 설치합니다:

flutter pub get

2. 테스트 파일 생성

테스트 파일은 test 디렉토리에 저장되며, 예시 이름은 api_service_test.dart입니다.

Mock 객체 생성 및 사용하기

1. Mock 클래스 생성

테스트할 클래스를 Mock 객체로 만들기 위해 mockitoMock 클래스를 확장합니다.

예를 들어 ApiService라는 클래스가 있다고 가정해 보겠습니다:

class ApiService {
  Future<String> fetchData() async {
    // 실제 네트워크 요청 로직
    return "Real Data";
  }
}

이 클래스를 테스트하기 위해 Mock 클래스를 생성합니다:

import 'package:mockito/mockito.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/api_service.dart';

// Mock 클래스 생성
class MockApiService extends Mock implements ApiService {}

2. Mock 객체 사용

Mock 객체를 사용하여 테스트를 작성하는 예제는 다음과 같습니다:

void main() {
  group('ApiService 테스트', () {
    // Mock 객체 선언
    late MockApiService mockApiService;

    setUp(() {
      mockApiService = MockApiService();
    });

    test('fetchData가 "Mock Data"를 반환하는지 확인', () async {
      // Mock 객체의 반환값 설정
      when(mockApiService.fetchData()).thenAnswer((_) async => "Mock Data");

      // 테스트 실행
      final result = await mockApiService.fetchData();

      // 결과 검증
      expect(result, "Mock Data");
    });
  });
}

주요 함수와 개념

1. whenthenAnswer

  • when: 특정 함수가 호출될 때 동작을 정의합니다.
  • thenAnswer: 비동기 함수의 반환값을 설정할 때 사용합니다.
  • 예:
    when(mockApiService.fetchData()).thenAnswer((_) async => "Mock Result");

2. verify

  • 특정 함수가 호출되었는지 확인할 수 있습니다.
  • 예:
    verify(mockApiService.fetchData()).called(1);

3. anycaptureAny

  • any: 인자 값에 관계없이 호출을 검증합니다.
  • captureAny: 인자 값을 캡처합니다.
  • 예:
    when(mockApiService.fetchData(any)).thenReturn("Mock Value");

모범 사례 및 팁

  1. 단일 동작 검증: 각 테스트 케이스는 하나의 동작만 검증하도록 작성하세요.
  2. 실제 클래스와 Mock 객체 분리: Mock 객체는 테스트 코드에서만 사용해야 합니다.
  3. 가짜 반환값 다양화: 다양한 시나리오(성공, 실패 등)를 가정하여 여러 반환값을 설정해보세요.
  4. setUptearDown 사용: 테스트 전후로 초기화 작업을 수행합니다.

Flutter Mocking의 활용 사례

  • 네트워크 API 테스트: 외부 API 호출 대신 Mock 데이터를 반환합니다.
  • 데이터베이스 검증: 데이터베이스 호출을 가짜 객체로 대체합니다.
  • UI 로직 테스트: 위젯 테스트에서 비즈니스 로직을 검증할 때 활용합니다.

결론

Mocking은 Flutter 단위 테스트에서 필수적인 기술입니다. mockito 패키지를 활용하면 복잡한 외부 의존성을 제거하고 더 빠르고 유연한 테스트를 작성할 수 있습니다. 앱의 품질을 높이기 위해 Mock 객체를 적극적으로 활용해 보세요.

 

Flutter 개발에서 단위 테스트와 Mocking을 완벽하게 마스터한다면 안정적이고 확장 가능한 앱을 개발할 수 있을 것입니다.

 

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

Future<Album> fetchAlbum(http.Client client) async {
  final response = await client
      .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));

  if (response.statusCode == 200) {
    // If the server did return a 200 OK response,
    // then parse the JSON.
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    // If the server did not return a 200 OK response,
    // then throw an exception.
    throw Exception('Failed to load album');
  }
}

class Album {
  final int userId;
  final int id;
  final String title;

  const Album({required this.userId, required this.id, required this.title});

  factory Album.fromJson(Map<String, dynamic> json) {
    return Album(
      userId: json['userId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
    );
  }
}

void main() => runApp(const MyApp());

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  late final Future<Album> futureAlbum;

  @override
  void initState() {
    super.initState();
    futureAlbum = fetchAlbum(http.Client());
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fetch Data Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Fetch Data Example'),
        ),
        body: Center(
          child: FutureBuilder<Album>(
            future: futureAlbum,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(snapshot.data!.title);
              } else if (snapshot.hasError) {
                return Text('${snapshot.error}');
              }

              // By default, show a loading spinner.
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}