때때로 단위 테스트는 실시간 웹 서비스 또는 데이터베이스에서 데이터를 가져오는 클래스에 의존할 수 있다. 이는 아래와 같은 이유들로 불편하다.
따라서 실시간 웹 서비스나 데이터베이스에 의존하는 대신 이러한 종속성을 "mock(모의)"할 수 있다. Mocks는 실시간 웹 서비스 또는 데이터베이스를 에뮬레이션하고 상황에 따라 특정 결과를 반환할 수 있습니다.
일반적으로 말하면 클래스의 대체 구현을 만들어 종속성을 모의할 수 있습니다. 이러한 대체 구현을 직접 작성하거나 Mockito 패키지를 바로 가기로 사용해라.
이 레시피는 다음 단계를 사용하여 Mockito 패키지로 모킹하는 기본 사항을 보여준다.
자세한 내용은 Mockito 패키지 설명서를 참조.
mockito 패키지를 사용하려면 dev_dependencies 섹션의 flutter_test 종속성 과 함께 .pubspec.yaml 파일에 추가해라.
이 예제에서 http 패키지도 사용하므로 dependencies 섹션에서 해당 종속성을 정의한다.
mockito: 5.0.0는 코드 생성 덕분에 Dart의 null safety를 지원한다. 필요한 코드 생성을 실행하려면 dev_dependencies 섹션에 build_runner 종속성을 추가해라.
종속성을 추가하려면 flutter pub add을 실행하자.
flutter pub add http dev:mockito dev:build_runner
이 예제에서는 Fetch data from the internet 레시피에서 fetchAlbum 함수를 단위 테스트한다. 이 기능을 테스트하려면 두 가지를 변경한다.
1. 함수에 http.Client를 제공해라. 이를 통해 상황에 따라 정확한 http.Client를 제공할 수 있다. Flutter 및 서버 측 프로젝트의 경우 http.IOClient를 제공하라. 브라우저 앱의 경우 http.BrowserClient를 제공하라. 테스트를 위해 mock의 http.Client를 제공하라.
2. 인터넷에서 데이터를 가져오려면 모의하기 어려운 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) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
앱 코드에서 기본 http.Client를 생성하는 fetchAlbum(http.Client()). http.Client()를 사용하여 fetchAlbum 메서드에 http.Client를 직접 제공할 수 있다.
다음으로 테스트 파일을 만든다.
단위 테스트 소개 레시피의 조언에 따라 루트 test 폴더에 fetch_album_test.dart라는 파일을 만든다.
@GenerateMocks([http.Client]) 주석을 mockito와 함께 MockClient 클래스를 생성하기 위한 매인 함수에 추가하라.
생성된 MockClient 클래스는 http.Client 클래스를 구현한다. 이를 통해 MockClient를 fetchAlbum 함수에 전달 하고 각 테스트에서 다른 http 응답을 반환할 수 있다.
생성된 mock은 fetch_album_test.mocks.dart에 있다. 사용하려면 해당 파일을 사용하기 위해 가져오자.
import 'package:http/http.dart' as http;
import 'package:mocking/main.dart';
import 'package:mockito/annotations.dart';
// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
([http.Client])
void main() {
}
다음으로 아래 명령어를 실행하여 mock을 생성하자.
flutter pub run build_runner build
이 fetchAlbum() 함수는 다음 두 가지 중 하나를 수행한다.
1. http 호출이 성공하면 Album 반환.
2. 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';
// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
([http.Client])
void main() {
group('fetchAlbum', () {
test('returns an Album if the http call completes successfully', () async {
final client = MockClient();
// Use Mockito to return a successful response when it calls the
// provided 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('throws an exception if the http call completes with an error', () {
final client = MockClient();
// Use Mockito to return an unsuccessful response when it calls the
// provided http.Client.
when(client
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
.thenAnswer((_) async => http.Response('Not Found', 404));
expect(fetchAlbum(client), throwsException);
});
});
}
이제 테스트가 포함된 fetchAlbum() 함수가 준비되었으므로 테스트를 실행한다.
flutter test test/fetch_album_test.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) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body));
} 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'],
id: json['id'],
title: json['title'],
);
}
}
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final Future<Album> futureAlbum;
void initState() {
super.initState();
futureAlbum = fetchAlbum(http.Client());
}
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
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();
},
),
),
),
);
}
}
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';
// Generate a MockClient using the Mockito package.
// Create new instances of this class in each test.
([http.Client])
void main() {
group('fetchAlbum', () {
test('returns an Album if the http call completes successfully', () async {
final client = MockClient();
// Use Mockito to return a successful response when it calls the
// provided 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('throws an exception if the http call completes with an error', () {
final client = MockClient();
// Use Mockito to return an unsuccessful response when it calls the
// provided http.Client.
when(client
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1')))
.thenAnswer((_) async => http.Response('Not Found', 404));
expect(fetchAlbum(client), throwsException);
});
});
}
이 예제에서는 Mockito를 사용하여 웹 서비스 또는 데이터베이스에 의존하는 함수 또는 클래스를 테스트하는 방법을 배웠다. 이것은 Mockito 라이브러리와 mocking의 개념에 대한 짧은 소개일 뿐이다. 자세한 내용은 Mockito 패키지에서 제공하는 설명서를 참조해라.