티스토리 뷰

반응형

단위 테스트에서 웹 서비스나 데이터베이스에서 데이터를 가져오는 클래스에 의존할 때가 있습니다. 이는 다음과 같은 이유로 불편할 수 있습니다:

  • 실제 서비스나 데이터베이스를 호출하면 테스트 실행 속도가 느려집니다.
  • 테스트가 통과하더라도, 웹 서비스나 데이터베이스가 예상치 못한 결과를 반환하면 테스트가 실패할 수 있습니다. 이를 '불안정한 테스트(flaky test)'라고 합니다.
  • 실제 서비스나 데이터베이스를 사용하면 모든 성공 및 실패 시나리오를 테스트하기 어렵습니다.

따라서 실제 웹 서비스나 데이터베이스에 의존하는 대신, 이러한 의존성을 "모킹(mock)"할 수 있습니다. 모킹은 실제 웹 서비스나 데이터베이스를 흉내 내어 상황에 따라 특정 결과를 반환할 수 있게 합니다.

 

일반적으로 클래스를 대체할 다른 클래스를 구현하여 의존성을 모킹할 수 있습니다. 하지만 Mockito와 같은 패키지를 사용하면 더 쉽게 의존성을 모킹할 수 있습니다.

 

이 레시피에서는 Mockito 패키지를 사용하여 모킹의 기본 개념을 설명하며, 다음 단계들을 다룹니다:

  1. 패키지 의존성 추가
  2. 테스트할 함수 작성
  3. 모킹된 http.Client를 사용하는 테스트 파일 작성
  4. 각 조건에 맞는 테스트 작성
  5. 테스트 실행

1. 패키지 의존성 추가

Mockito 패키지를 사용하려면, pubspec.yaml 파일의 dev_dependencies 섹션에 mockitoflutter_test 의존성을 추가합니다.

 

이 예제는 http 패키지도 사용하므로 dependencies 섹션에 이를 정의합니다.

 

Mockito 5.0.0은 코드 생성 덕분에 Dart의 null-safety를 지원합니다. 필요한 코드 생성을 실행하려면 build_runner 의존성을 dev_dependencies에 추가하세요.

 

의존성을 추가하려면 다음 명령어를 실행하세요:

flutter pub add http dev:mockito dev:build_runner

2. 테스트할 함수 작성

이 예제에서는 Fetch data from the internet 레시피의 fetchAlbum 함수를 단위 테스트합니다. 테스트를 위해 두 가지 변경사항이 필요합니다:

  • 함수에 http.Client를 전달합니다. 이로 인해 상황에 맞는 적절한 http.Client를 제공할 수 있습니다. Flutter 및 서버 프로젝트에서는 http.IOClient를, 브라우저 앱에서는 http.BrowserClient를 사용하고, 테스트에서는 모킹된 http.Client를 제공합니다.
  • 정적인 http.get() 메서드를 사용하는 대신, 제공된 클라이언트를 사용하여 데이터를 가져오도록 변경합니다. 정적 메서드는 모킹하기 어렵기 때문입니다.

변경된 함수는 다음과 같습니다:

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

  if (response.statusCode == 200) {
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    throw Exception('앨범을 로드하는 데 실패했습니다');
  }
}

앱 코드에서 fetchAlbum 메서드에 http.Client를 직접 전달할 수 있습니다: fetchAlbum(http.Client()). http.Client()는 기본 클라이언트를 생성합니다.

3. 모킹된 http.Client를 사용하는 테스트 파일 작성

다음으로, 테스트 파일을 작성합니다.

 

test 폴더에 fetch_album_test.dart라는 파일을 생성하세요. 메인 함수에 @GenerateMocks([http.Client]) 주석을 추가하여 Mockito로 MockClient 클래스를 생성합니다.

 

생성된 MockClient 클래스는 http.Client 클래스를 구현합니다. 이를 통해 MockClientfetchAlbum 함수에 전달하고, 각 테스트에서 다른 http 응답을 반환할 수 있습니다.

 

생성된 모킹 클래스는 fetch_album_test.mocks.dart 파일에 위치합니다. 이를 가져와서 사용하세요.

import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';

// Mockito 패키지를 사용하여 MockClient 생성
// 각 테스트에서 이 클래스의 새 인스턴스를 생성
@GenerateMocks([http.Client])
void main() {
}

 

다음 명령어로 모킹 클래스를 생성합니다:

dart run build_runner build

4. 각 조건에 맞는 테스트 작성

fetchAlbum() 함수는 다음 두 가지 중 하나를 수행합니다:

  • http 호출이 성공하면 Album을 반환합니다.
  • http 호출이 실패하면 Exception을 던집니다.

따라서, 이 두 조건을 테스트해야 합니다. MockClient 클래스를 사용하여 성공 테스트에서는 "OK" 응답을, 실패 테스트에서는 오류 응답을 반환하세요. 이 조건들을 Mockito에서 제공하는 when() 함수를 사용하여 테스트할 수 있습니다:

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'fetch_album_test.mocks.dart';

// Mockito 패키지를 사용하여 MockClient 생성
// 각 테스트에서 이 클래스의 새 인스턴스를 생성
@GenerateMocks([http.Client])
void main() {
  group('fetchAlbum', () {
    test('http 호출이 성공하면 Album을 반환해야 합니다', () async {
      final client = MockClient();

      // Mockito를 사용하여 제공된 http.Client가 호출될 때 성공 응답을 반환
      when(client
              .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
          .thenAnswer((_) async =>
              http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200));

      expect(await fetchAlbum(client), isA<Album>());
    });

    test('http 호출이 실패하면 예외를 던져야 합니다', () {
      final client = MockClient();

      // Mockito를 사용하여 제공된 http.Client가 호출될 때 실패 응답을 반환
      when(client
              .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchAlbum(client), throwsException);
    });
  });
}

5. 테스트 실행

이제 fetchAlbum() 함수와 관련된 테스트가 준비되었으므로, 테스트를 실행할 수 있습니다.

flutter test test/fetch_album_test.dart

또는, 단위 테스트 소개 레시피의 지침에 따라 좋아하는 에디터 내에서 테스트를 실행할 수 있습니다.

전체 예제

lib/main.dart

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) {
    return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
  } else {
    throw Exception('앨범을 로드하는 데 실패했습니다');
  }
}

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: '데이터 가져오기 예제',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('데이터 가져오기 예제'),
        ),
        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}');
              }
              return const CircularProgressIndicator();
            },
          ),
        ),
      ),
    );
  }
}

test/fetch_album_test.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';

import 'fetch_album_test.mocks.dart';

// Mockito 패키지를 사용하여 MockClient 생성
// 각 테스트에서 이 클래스의 새 인스턴스를 생성
@GenerateMocks([http.Client])
void main() {
  group('fetchAlbum', () {
    test('http 호출이 성공하면 Album을 반환해야 합니다', () async {
      final client = MockClient();

      // Mockito를 사용하여 성공 응답 반환
      when(client
              .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
          .thenAnswer((_) async =>
              http.Response('{"userId": 1, "id": 2, "title": "mock"}', 200));

      expect(await fetchAlbum(client), isA<Album>());
    });

    test('http 호출이 실패하면 예외를 던져야 합니다', () {
      final client = MockClient();

      // Mockito를 사용하여 실패 응답 반환
      when(client
              .get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
          .thenAnswer((_) async => http.Response('Not Found', 404));

      expect(fetchAlbum(client), throwsException);
    });
  });
}

요약

이 예제에서는 웹 서비스나 데이터베이스에 의존하는 함수나 클래스를 테스트하기 위해 Mockito를 사용하는 방법을 배웠습니다. 이는 Mockito 라이브러리와 모킹 개념에 대한 짧은 소개입니다. 더 자세한 정보는 Mockito 패키지 문서를 참고하세요.

반응형

'Flutter > 테스팅' 카테고리의 다른 글

플러터] 단위 테스팅 소개  (0) 2024.09.21
플러터] 플러터 앱 테스트 개요  (0) 2024.09.21
댓글
공지사항