티스토리 뷰
단위 테스트에서 웹 서비스나 데이터베이스에서 데이터를 가져오는 클래스에 의존할 때가 있습니다. 이는 다음과 같은 이유로 불편할 수 있습니다:
- 실제 서비스나 데이터베이스를 호출하면 테스트 실행 속도가 느려집니다.
- 테스트가 통과하더라도, 웹 서비스나 데이터베이스가 예상치 못한 결과를 반환하면 테스트가 실패할 수 있습니다. 이를 '불안정한 테스트(flaky test)'라고 합니다.
- 실제 서비스나 데이터베이스를 사용하면 모든 성공 및 실패 시나리오를 테스트하기 어렵습니다.
따라서 실제 웹 서비스나 데이터베이스에 의존하는 대신, 이러한 의존성을 "모킹(mock)"할 수 있습니다. 모킹은 실제 웹 서비스나 데이터베이스를 흉내 내어 상황에 따라 특정 결과를 반환할 수 있게 합니다.
일반적으로 클래스를 대체할 다른 클래스를 구현하여 의존성을 모킹할 수 있습니다. 하지만 Mockito와 같은 패키지를 사용하면 더 쉽게 의존성을 모킹할 수 있습니다.
이 레시피에서는 Mockito 패키지를 사용하여 모킹의 기본 개념을 설명하며, 다음 단계들을 다룹니다:
- 패키지 의존성 추가
- 테스트할 함수 작성
- 모킹된
http.Client
를 사용하는 테스트 파일 작성 - 각 조건에 맞는 테스트 작성
- 테스트 실행
1. 패키지 의존성 추가
Mockito 패키지를 사용하려면, pubspec.yaml
파일의 dev_dependencies
섹션에 mockito
와 flutter_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
클래스를 구현합니다. 이를 통해 MockClient
를 fetchAlbum
함수에 전달하고, 각 테스트에서 다른 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 |