티스토리 뷰

Flutter

Flutter의 Unit Test (Mockito 패키지)

순진이 2024. 7. 26. 19:38

Flutter의 Test에는 총 3가지의 테스트가 있음!

 

Unit Test

간단한 함수나 클래스와 같이 작은 단위를 테스트

 

Widget Test

말 그대로 위젯을 테스트. 위젯의 UI가 예상대로 표시되고 있는지? 이벤트에는 적절하게 응답하는지 확인 가능

 

Intergration Test

전체 앱의 흐름을 테스트할 수 있음. 모든 위젯과 서비스가 예상대로 작동하는지 확인하는 테스트. 앱의 성능을 측정할 수 있음

 

이 중에 가장 기초가 되는 Unit Test 하는 방법을 알아보겠음.

Unit Test의 경우 Mockito이라는 패키지를 이용해서 통신과 관련된 함수들 또한 테스트 해볼 수 있음!

진짜 통신을 하지 않아도 내가 만든 API 통신 함수가 성공이나 실패 시 내가 설계해놓은대로 결과를 return하는지 확인할 수 있음!

 


테스트 준비하기

테스트를 위해서 몇 가지 준비 사항이 있음

1. 패키지 설치

 

일단 Flutter의 테스트를 위해서는 test라는 패키지를 pub get 해야 함

개발 시에만 필요한 패키지이므로 dev로 받아주면 됨

flutter pub add dev:test
 

test install | Dart package

A full featured library for writing and running Dart tests across platforms.

pub.dev

 

그리고 Mockito 패키지를 사용하려면 build_runner와 mockito 패키지도 설치해야함

아래 명령어를 통하면 필요한 3가지 모두 가져올 수 있음

flutter pub add dev:mockito dev:build_runner dev:test

 

 

mockito | Dart package

A mock framework inspired by Mockito with APIs for Fakes, Mocks, behavior verification, and stubbing.

pub.dev

 

 

build_runner | Dart package

A build system for Dart code generation and modular compilation.

pub.dev

 

아, 그리고 통신에는 Dio를 사용할 예정이므로, Dio 패키지도 받아주면 됨

flutter pub add dio

2. test 폴더 만들기

lib과 같은 수준에 test라는 폴더를 만들어줘야 함.

그리고 테스트할 파일들은 반드시 test로 끝나야 함

 

내 프로젝트 이름이 test_practice라면, 아래와 같은 구조가 될 것임

-test_practice

    -lib/

         -counter.dart

         -fetch_data.dart

    -test/

         -counter_test.dart

         -fetch_data_test.dart

 


3. 테스트는 반드시 main()에서 실행해줘야 함

테스트 폴더 아래 테스트 파일을 만들고 이를 실행할 때 void main() {} 함수를 만들어 그 함수 내에서 실행해야 함


간단한 함수 테스트

우선은 간단한 함수 테스트부터 해보겠음

※ 이 부분은 너무 간단해서 Mock 테스트만 봐도 사실 문제 없음  

 

이를 위해서 Counter라는 클래스를 아래와 같이 만들어주고, 값을 올리고 더하는 간단한 함수 2개를 만들어 주갔음

class Counter {
  int value = 0;

  void increment() => value++;
  void decrement() => value--;
}

 

 

이제 이 놈을 테스트 하기 위해서 test 폴더 아래 counter_test.dart라는 파일을 만들어주고, 준비사항 3번에서 설명한 것처럼 main() 함수를 만들어줌

 

 

테스트는 한마디로 말하자면 기대값과 실제로 함수가 내뱉는 값이 일치하면 성공임

test는 1개만 진행할 수도 있고 그룹으로 진행할 수도 있음

 

 

1. 개별 테스트

test 함수를 이용해주면 됨!

우선 description에 테스트 이름을 적고, 실행 내용을 body에 적어주면 되는데, 나는 클래스 내 함수이므로 클래스의 객체를 만들었음

그리고 increment() 함수를 실행했을 때, 기대되는 값과 실제 결과값을 비교해서 같으면 테스트가 성공임!

 

expect는 두가지 인자를 받는데, 첫 번째는 테스트하려는 실제 값이고 두 번째는 기대되는 값임

expect(테스트에서 나온 실제 값, 기대값)

 

 

Counter 함수의 increment()를 1번만 실행하면, value가 +1이 되어 1을 기대값으로 적어주었다

import 'package:test/test.dart';
import 'package:test_practice/counter.dart';

void main() {
  test(
    'counter test',
    () {
      final counter = Counter();
      counter.increment();
      expect(counter.value, 1);
    },
  );
}

 

 

그리고 이제 테스트를 진행하면 되는데, 방법은 터미널에서 아래와 같은 명령어를 적어주면 됨

test 폴더 아래 counter_test.dart를 테스트하겠다는 의미임!

flutter test test/counter_test.dart

 

또는 아래처럼 test 이름으로 실행해도 됨

flutter test --plain-name "counter test"

 

그러면 아래처럼 성공했다고 알려줌!

 

 

2. 그룹 테스트

저런 개별 테스트를 그룹으로 만들어 할 수도 있음!

구조는 test와 별반 다르지 않음. 

description에  테스트 이름을 적고, 테스트들을 body에 넣어주면 됨

함수를 여러번 실행할 수도 있음

import 'package:test/test.dart';
import 'package:test_practice/counter.dart';

void main() {
  group(
    'increment, decrement test',
    () {
      test(
        'value 1',
        () {
          final counter = Counter();
          counter.increment();
          expect(counter.value, 1);
        },
      );

      test(
        'value 2',
        () {
          final counter = Counter();
          counter.increment();
          counter.increment();
          expect(counter.value, 2);
        },
      );

      test(
        'value 0',
        () {
          final counter = Counter();
          counter.increment();
          counter.decrement();
          expect(counter.value, 0);
        },
      );
    },
  );
}

 

이 코드 또한 터미널에서 실행해주면! 성공!!!

flutter test test/counter_test.dart
flutter test --plain-name "increment, decrement test"

 


Mockito 패키지 사용 테스트

 

이번에는 Mockito 패키지를 가지고 테스트를 해보겠음!

Mockito 패키지는 서버와 통신을 하지 않고도 서버 통신 함수를 테스트할 수 있도록 도와줌

실제 Dio, http 같은 객체 대신에 모의 객체(Mock)을 이용하여 테스트 하기 때문임!

 

우선 준비사항을 다 체크한 후, 아래와 같은 통신 코드를 만들어줌

jsonplaceholder라는 사이트에서 무료로 제공해주는 api임

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

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

class ApiService {
  final Dio dio;

  ApiService({required this.dio});

  Future<Album> fetchAlbum() async {
    try {
      final response =
          await dio.get('https://jsonplaceholder.typicode.com/albums/1');
      if (response.statusCode == 200) {
        return Album.fromJson(response.data);
      } else {
        throw HttpException('Failed to load album');
      }
    } catch (e) {
      throw HttpException('Failed to load album');
    }
  }
}

 

 

그리고 test 폴더 아래 테스트 파일을 만들어줌

나는 fetch_test.dart라고 만들었음

 

그리고 이번에는 아래와 같이 만들어줌

GenerateMocks라는 어노테이션을 붙여주고 Dio를 사용할 것이므로 Dio 적어주면 됨!

import 'package:dio/dio.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([Dio])
void main() {}

 

그리고 터미널에서 build_runner를 실행해줌!

dart run build_runner build

 

그러면 내가 만든 테스트 파일 뒤에 mock라는 이름이 붙은 파일이 하나 더 생성됨! 

 

 

mock 파일이 만들어졌으면 내가 만든 테스트 파일(fetch_test.dart)에서 테스트를 해주면 됨!

우선 통신하는 코드가 있는 ApiService 클래스와 MockDio 객체롤 late 키워드로 만들어 준 후 setUp에서 객체를 할당해줌

 

setUp 함수는 테스트들이 실행되기 전에 실행되는 함수이기 때문에 저기에서 하면 됨!

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:mockito/annotations.dart';
import 'package:test/test.dart';
import 'package:mockito/mockito.dart';
import 'package:test_practice/fetch_data.dart';

import 'fetch_data_test.mocks.dart';

@GenerateMocks([Dio])
void main() {
  late ApiService apiService;
  late MockDio mockDio;

  setUp(
    () {
      mockDio = MockDio();
      apiService = ApiService(dio: mockDio);
    },
  );
}

 

여기서는 when이라는 메서드를 사용할 예정인데, MockDio에서 제공하는 메서드로 모의객체(MockDio)의 메서드(get)를 호출할 수 있도록 도와줌. 

 

 

이를 통해 호출한 값과 결과값을 비교해서 다르면 테스트 실패, 같으면 테스트 성공!

 

 

통신을 성공한 케이스와 실패한 케이스 두 가지의 테스트를 할 예정임

 

 

1. 성공의 경우

      test(
        'mockito test 1 - success',
        () async {
          final responsePayload = {"userId": 1, "id": 1, "title": 'supernova'};
          when(mockDio.get('https://jsonplaceholder.typicode.com/albums/1'))
              .thenAnswer(
            (_) async {
              return Response(
                  requestOptions: RequestOptions(path: ''),
                  data: responsePayload,
                  statusCode: 200);
            },
          );

          final response = await apiService.fetchAlbum();
          expect(response, isA<Album>());
          expect(response.id, 1);
          expect(response.userId, 1);
          expect(response.title, 'supernova');
          verify(mockDio.get('https://jsonplaceholder.typicode.com/albums/1'))
              .called(1);
        },
      );

 

when을 통해 메서드를 호출했다면, 이제 thenAnswer을 통해 어떤 값을 반환할지를 정하는 단계임. 

(thenReturn은 간단한 반환값에 사용되고 thenAnswer는 비동기 작업이 필요한 경우 사용됨)

 

나는(나만) 이 부분이 이해가 안갔는데

실제 Dio가 아닌 MockDio를 통해 fetchAlbum() 함수를 실행했으니, 그 반환값(response)을 "내가" 정해주는 것임.

 

 

"내가 설정한 Response"를 리턴할 fetchAlbum()의 실제값과 기대값을 비교해서 테스트 성공 여부가 정해짐!

fetchAlbum()에서 statusCode 200일 경우 Album 타입의 데이터를 리턴했었음. 

그리고 내가 설정한 data인 {"id": 1, "userId": 1, "title": "superNoba"}가 결과로 와야 맞음

 

 

이제 Album 타입의 데이터 객체를 expect를 통해 기대값과 비교해보면 됨!

위에서 말했던 것처럼 expect(실제값, 기대값)이므로 아래처럼 테스를 해볼 수 있음

 

 

- expect(response, isA<Album>());

: response의 타입을 확인하는 부분임. Album 타입을 return했으므로 기대값은 Album임

 

 

- expect(response.id, 1);

- expect(response.userId, 1);

- expect(response.title, "supernova");

: 내가 넣은 data 값과 기대값도 비교해볼 수 있음

 

 

- verify(mockDio.get('https://jsonplaceholder.typicode.com/albums/1')).called(1);

: 특정 메서드 호출이 실제로 일어났는지 확인할 수 있음. 

called(1)을 통해 해당 메서드가 몇 번 호출됐는지 확인 가능

 

 


2. 실패의 경우

실패의 경우도 성공과 동일하다고 할 수 있음

여기서 다른 점은 thenAnswer 대신 thenThrow라는 메서드를 써주는 것인데, 이는 MockDio가 예외를 던지도록 해줘서 예외 상황을 만들 수 있게 해줌

      test(
        'mockito test 2 - failure',
        () async {

          when(mockDio.get('https://jsonplaceholder.typicode.com/albums/1'))
              .thenThrow(DioException(
            requestOptions: RequestOptions(path: ''),
            response: Response(
              requestOptions: RequestOptions(path: ''),
              statusCode: 404,
              data: {},
            ),
            type: DioExceptionType.badResponse,
          ));

          expect(apiService.fetchAlbum(), throwsA(isA<HttpException>()));
          expect(
              apiService.fetchAlbum(),
              throwsA(predicate((e) =>
                  e is HttpException && e.message == 'Failed to load album')));
        },
      );

 

fetchAlbum()에서는 statusCode가 200이 아닌 경우 throw HttpException('Failed to load album');을 던지도록 만들어놨었기 때문에 그 예상값(HttpException)과 실제 나온 값을 비교하면 되겠음

 

 

- expect(apiService.fetchAlbum(), throwsA(isA()));

: 던져진 예외의 타입을 테스트

※ 예외의 타입을 확인할 때는 throwsA을 써야 함

 

 

-expect(apiService.fetchAlbum(), throwsA(predicate((e) => e is HttpException && e.message == 'Failed to load album')));

: 던져진 예외 타입과 그 예외 메시지를 테스트

 

 

그 외에 Exception을 커스텀으로 만들어서 쓰는 경우도 많음!

그럴 때는 예상 값에 해당 CustomException을 넣어주면 됨!

 


이렇게 간단하게 플러터에서 Unit 테스트를 하는 방법을 알아보았음.

MockDio 덕분에 실제 통신이 없이도 통신 코드를 점검해볼 수 있다는 점이 큰 장점이지 싶다!

 

'Flutter' 카테고리의 다른 글

[간단 Tip] Barrel, part, part of  (0) 2024.07.19
ProxyProvider 간단 사용 예제  (0) 2024.07.16
try-catch 정리해보기  (0) 2024.07.11
addPostFrameCallback를 사용한 error 해결  (0) 2024.07.09
Form 필드로 유효성 체크하기  (1) 2024.07.02
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크