[Flutter] Mockito를 활용한 테스트

Comely·2024년 11월 16일

Flutter

목록 보기
13/26

Flutter 문서 Mockito

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

http 호출이 성공하면 앨범을 반환합니다.
http 호출이 실패하면 예외를 throw합니다.
따라서 이 두 가지 조건을 테스트하고 싶습니다.
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를 생성합니다.
// 각 테스트에서 이 클래스의 새 인스턴스를 생성합니다.
([http.Client])
void main() {
  group('fetchAlbum', () {
    test('http 호출이 성공적으로 완료되면 Album을 반환합니다.', () async {
      final client = MockClient();

      // 제공된 http.Client가 호출될 때 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();

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

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

1. Mockito를 활용한 Pixabay 테스트

테스트의 주요 목적

  1. API 호출 로직 검증
    클라이언트가 올바른 URL로 호출을 수행하는지 확인합니다.
  2. 응답 데이터 처리 검증
    JSON 응답을 올바르게 파싱하여 Photo 객체 리스트를 생성하는지 확인합니다.
  3. HTTP 요청 최소화
    테스트 환경에서 실제 API를 호출하지 않고, 네트워크 의존성을 제거합니다.

Pixabay API 클래스

class PixabayApi {
  final http.Client client;

  PixabayApi(this.client);

  static const baseUrl = 'https://pixabay.com/api/';
  static const key = '10711147-dc41758b93b263957026bdadb';

  Future<Result<Iterable>> fetch(String query) async {
    try {
      final response = await client
          .get(Uri.parse('$baseUrl?key=$key&q=$query&image_type=photo'));
      Map<String, dynamic> jsonResponse = jsonDecode(response.body);
      Iterable hits = jsonResponse['hits'];
      return Result.success(hits);
    } catch (e) {
      return const Result.error('네트워크 에러');
    }
  }
}

PixabayApi 클래스는 API 요청을 처리하는 역할을 합니다.
테스트에서는 client.get 메서드의 결과를 JSON으로 파싱하고, 이를 Photo 객체 리스트로 변환합니다.

pixabay_api_test.dart

import 'pixabay_api_test.mocks.dart';

([http.Client])
void main() {
  test('Pixabay 데이터를 잘 가져와야 한다', () async {
    final client = MockClient();
    final api = PhotoApiRepositoryImpl(PixabayApi(client));

    when(client.get(Uri.parse(
            '${PixabayApi.baseUrl}?key=${PixabayApi.key}&q=iphone&image_type=photo')))
        .thenAnswer((_) async => http.Response(fakeJsonBody, 200));

    final Result<List<Photo>> result = await api.fetch('iphone');

    expect((result as Success<List<Photo>>).data.first.id, 410311);

    verify(client.get(Uri.parse(
        '${PixabayApi.baseUrl}?key=${PixabayApi.key}&q=iphone&image_type=photo')));
  });
}

String fakeJsonBody = """
{"total":1320,"totalHits":500,"hits":[{"id":410311,"pageURL":"https://pixabay.com/photos/iphone-hand-screen-smartphone-apps-410311/","type":"photo","tags":"iphone, hand, screen","previewURL":"https://cdn.pixabay.com/photo/2014/08/05/10/27/iphone-410311_150.jpg","previewWidth":150,"previewHeight":99,"webformatURL":"https://pixabay.com/get/ga40944969ab1ca0cb5e5e2a753382c5ef38aa9b1bdf195f44a6e8c7def03f5b2ce08c74211f5bd254565642907f5e7b5_640.jpg","webformatWidth":640,"webformatHeight":426,"largeImageURL":"https://pixabay.com/get/gac97151d90f6f74f39ba9a6013d97a3e0c8b3b2673356bef20a65b9a253d439913d8d3566a6e8485773b9aea90170c38a538a3582b0a2af3e51efe53ebc8885b_1280.jpg","imageWidth":1920,"imageHeight":1280,"imageSize":416413,"views":441374,"downloads":213676,"collections":2913,"likes":573,"comments":146,"user_id":264599,"user":"JESHOOTS-com","userImageURL":"https://cdn.pixabay.com/user/2014/06/08/15-27-10-248_250x250.jpg"},{...} 생략
]

1. 가짜 JSON 데이터 (fakeJsonBody)

Pixabay API가 반환할 것 같은 응답 형식을 그대로 유지하면서 필요한 데이터만 포함합니다. 예: id, tags, previewURL 등.

String fakeJsonBody = """
{"total":1320,"totalHits":500,"hits":[{"id":410311, ...}]
""";

2. HTTP 클라이언트 모킹

MockClient는 http 패키지에서 제공하는 클라이언트를 모킹하기 위한 도구입니다.
실제 HTTP 요청을 보내지 않고, fakeJsonBody를 반환하도록 설정합니다.

test('pixabay 이미지 검색 테스트', () async {
  final client = MockClient();

3. MockClient 설정

when(client.get(any))는 모든 GET 요청에 대해 응답을 모킹합니다.
thenAnswer를 사용해 JSON 응답(fakeJsonBody)과 HTTP 상태 코드(200)를 반환하도록 설정합니다.

when(client.get(any)).thenAnswer(
  (_) async => http.Response(fakeJsonBody, 200),
);

4. API 호출 및 검증

PixabayApi 클래스의 fetchImages 메서드를 호출합니다.
fetchImages는 API를 통해 "iphone" 키워드로 이미지를 검색하는 메서드입니다.

final result = await PixabayApi(client).fetchImages('iphone');

호출 결과가 Success 타입이어야 하며, 첫 번째 이미지의 id가 410311인지 확인합니다.
이는 fakeJsonBody에 정의된 데이터를 기반으로 올바르게 처리되었는지 검증하는 과정입니다.

expect((result as Success<List<Photo>>).data.first.id, 410311);

실제 호출된 URL이 예상한 것과 일치하는지 확인합니다.

verify(client.get(Uri.parse(
  '${PixabayApi.baseUrl}?key=${PixabayApi.key}&q=iphone&image_type=photo')));

2.HTTP의 MockClient

이 테스트는 Pixabay API의 기능을 가상으로 흉내 내어 정상적으로 동작하는지 확인하기 위해 작성되었습니다.
특히 네트워크 호출 없이도 API 로직을 검증할 수 있도록 MockClient와 fakeJsonBody를 활용한 점이 특징입니다.

전체 테스트 흐름

  • mockClient 생성 : 요청에 따라 미리 정의된 응답을 반환하도록 설정.
  • PixabayApi 인스턴스 생성 : mockClient를 사용하도록 설정.
  • fetch 호출 : 키워드 "iphone"에 대한 이미지를 검색.
  • 결과 검증 : 첫 번째 이미지의 URL과 이미지 리스트의 개수를 예상값과 비교.
import 'package:http/testing.dart';

void main() {
  test('Pixabay 이미지를 잘 가져와야 한다', () async {
    final mockClient = MockClient((request) async{
      if (request.url.toString() ==
          '${PixabayApi.baseUrl}?key=${PixabayApi.key}&q=iphone&image_type=photo') {
        return http.Response(fakeJsonBody, 200);
      }
      return http.Response('Not Found', 404);
    });
    final api = PixabayApi(client: mockClient));

    //실행
    List<Photo> photos = await api.fetch('iphone');

    //검증
    expect(photos[0].previewUrl,
        'https://cdn.pixabay.com/photo/2014/08/05/10/27/iphone-410311_150.jpg');
    expect(photos.length, 20);
  });
}


const fakeDate = """
{"total":8794,"totalHits":500,"hits":[{"id":634572,"pageURL":"https://pixabay.com/photos/iphone-6-apple-ios-634572/","type":"photo","tags":"iphone 6, apple, ios","previewURL":"https://cdn.pixabay.com/photo/2014/08/05/10/27/iphone-410311_150.jpg","previewWidth":150,"previewHeight":99,"webformatURL":"https://pixabay.com/get/52e1d3434c51ae14f6da8c7dda79367b113adde04e507441722a7cd6904bc658_640.jpg","webformat}
""";

코드 분석 및 설명

1. test() 함수

test('Pixabay 이미지를 잘 가져와야 한다', () async {
  • test()는 Dart의 유닛 테스트를 작성할 때 사용하는 함수입니다.
  • 첫 번째 인자는 테스트의 이름이며, 두 번째 인자는 테스트 로직을 포함한 콜백 함수입니다.
  • 이 테스트의 이름은 "Pixabay 이미지를 잘 가져와야 한다"입니다.

2. MockClient 생성

final mockClient = MockClient((request) async {

MockClient는 API 호출을 가상으로 흉내 내는 역할을 합니다.
이를 통해 실제 네트워크 호출 없이 미리 정의된 응답을 반환하도록 설정할 수 있습니다.
request는 http.Request 객체를 의미하며, 요청에 따라 다른 응답을 반환할 수 있도록 설정합니다.

3. MockClient의 조건 설정

if (request.url.toString() ==
    '${PixabayApi.baseUrl}?key=${PixabayApi.key}&q=iphone&image_type=photo') {
  return http.Response(fakeJsonBody, 200);
}
return http.Response('Not Found', 404);

요청 URL이 Pixabay API의 검색 URL과 정확히 일치할 경우:
미리 정의된 JSON 데이터(fakeJsonBody)를 HTTP 응답으로 반환합니다.
상태 코드는 200(정상 응답)을 반환합니다.

요청 URL이 일치하지 않을 경우:
'Not Found' 메시지와 상태 코드 404(페이지 없음)을 반환합니다.

4. PixabayApi 객체 생성

final api = PixabayApi(client: mockClient));

PixabayApi 클래스의 인스턴스를 생성하면서, client로 mockClient를 전달합니다.
이를 통해 PixabayApi가 실제 네트워크 호출 대신 mockClient를 통해 요청과 응답을 처리하도록 설정합니다.

5. API 호출 및 결과 저장

List<Photo> photos = await api.fetch('iphone');

PixabayApi 클래스의 fetch() 메서드를 호출하여 키워드 "iphone"에 해당하는 이미지 목록을 요청합니다.
요청 결과는 List 형태로 반환됩니다.
이 테스트에서는 mockClient에 의해 미리 정의된 fakeJsonBody가 반환되므로 실제 네트워크 호출은 발생하지 않습니다.

6. 결과 검증

expect(photos[0].previewUrl,
    'https://cdn.pixabay.com/photo/2014/08/05/10/27/iphone-410311_150.jpg');
expect(photos.length, 20);

expect()는 테스트에서 예상되는 결과를 검증하는 함수입니다.
첫 번째 expect : 첫 번째 이미지의 previewUrl 속성이 예상된 URL과 일치하는지 확인합니다.
두 번째 expect : 반환된 이미지 리스트의 길이가 20인지 확인합니다.

profile
App, Web Developer

0개의 댓글