플러터 Testing - Unit(2)

Inyeong Kang·2023년 7월 7일
0

Flutter Cookbook 링크

Mock dependencies using Mockito

때때로 단위 테스트는 실시간 웹 서비스 또는 데이터베이스에서 데이터를 가져오는 클래스에 의존할 수 있다. 이는 아래와 같은 이유들로 불편하다.

  • 실시간 서비스 또는 데이터베이스를 호출하면 테스트 실행 속도가 느려진다.
  • 웹 서비스 또는 데이터베이스가 예기치 않은 결과를 반환하면 통과 테스트가 실패하기 시작할 수 있다. 이것은 "flaky test(비정상적 테스트)"로 불린다.
  • 실시간 웹 서비스 또는 데이터베이스를 사용하여 가능한 모든 성공 및 실패 시나리오를 테스트하는 것은 어렵다.

따라서 실시간 웹 서비스나 데이터베이스에 의존하는 대신 이러한 종속성을 "mock(모의)"할 수 있다. Mocks는 실시간 웹 서비스 또는 데이터베이스를 에뮬레이션하고 상황에 따라 특정 결과를 반환할 수 있습니다.

일반적으로 말하면 클래스의 대체 구현을 만들어 종속성을 모의할 수 있습니다. 이러한 대체 구현을 직접 작성하거나 Mockito 패키지를 바로 가기로 사용해라.

이 레시피는 다음 단계를 사용하여 Mockito 패키지로 모킹하는 기본 사항을 보여준다.

  1. 패키지 종속성 추가
  2. 테스트 함수 생성
  3. mock http.Client로 테스트 파일 생성
  4. 각 조건에 대한 테스트 작성
  5. 테스트 실행

자세한 내용은 Mockito 패키지 설명서를 참조.

1. 패키지 종속성 추가

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

2. 테스트 함수 생성

이 예제에서는 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를 직접 제공할 수 있다.

3. mock 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

4. 각 조건에 대한 테스트 작성

이 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);
    });
  });
}

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) {
    // 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();
            },
          ),
        ),
      ),
    );
  }
}
  • 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';

// 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 패키지에서 제공하는 설명서를 참조해라.

profile
안녕하세요. 강인영입니다. GDSC에서 필요한 것들을 작업하고 업로드하려고 합니다!

0개의 댓글