본문 바로가기
Mobile Programming

Flutter(Dart) - http 패키지 사용법 및 유닛테스트

by 맑은안개 2022. 3. 18.

들어가며..

내/외부 자원을 얻기위해 대부분의 Application은 http 프로토콜을 사용하여 자원을 취득한다. Flutter는 http 프로토콜 위한 http패키지를 제공 한다. 이를 통해 Rest 서버 자원을 쉽게 얻을 수 있다.
본 장에서는 http 패키지를 사용하여 jsonplaceholder에서 제공하는 데이터를 얻는 간단한 API관리 객체를 만들어 본다. 처리되는 APIjson serialization 패키지를 사용하여 get/set 함수를 통해 json데이터 포맷으로 관리된다. 이 과정은 다루지 않으므로 필요하다면 해당 블로그를 확인한다.

개발환경

  • windows 10 ( MacOS 무관 )
  • Visual Studio Code
  • Flutter 2.10 ( Dart만 사용해도 무방하다. )

프로젝트 생성

flutter create exam_http

⚙ pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.4  
  json_annotation: ^4.4.0

dev_dependencies:
  test: ^1.16.4
  flutter_test:
    sdk: flutter
  build_runner: ^2.0.5
  json_serializable: ^6.1.5
  flutter_lints: ^1.0.0
  • http - http 처리 함수 제공 패키지
  • test - Dart unit test 제공 패키지
  • build_runner, json_annotation, json_serializable - Json 모델링 처리 패키지

Sample Model

https://jsonplaceholder.typicode.com/posts 데이터

📃 lib/model/post.dart

import 'package:json_annotation/json_annotation.dart';

part 'post.g.dart';

@JsonSerializable()
class Post {
  final int userId;
  final int id;
  final String title;
  final String body;

  Post(this.userId, this.id, this.title, this.body);

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);

  Map<String, dynamic> toJson() => _$PostToJson(this);

  @override
  String toString() {
    return "Post id [${id}] title: $title";
  }
}

📃 lib/model/post.g.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'post.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Post _$PostFromJson(Map<String, dynamic> json) => Post(
      json['userId'] as int,
      json['id'] as int,
      json['title'] as String,
      json['body'] as String,
    );

Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
      'userId': instance.userId,
      'id': instance.id,
      'title': instance.title,
      'body': instance.body,
    };
  • post.g.dart 파일은 build_runner를 통해 자동생성된 파일로서, @JsonSerializable어노테이션이 명시된 모델클래스의 Json매핑 함수의 모음이다. 자세한 사항은 다음블로그를 확인.

이제 api관리 객체를 생성해보자.

📃 lib/api/fake_post_api.dart

import 'package:exam_http/model/post.dart';
import 'dart:convert' as convert;
import 'package:http/http.dart' as http;

class FakePostApi {
  static const String baseUri = 'jsonplaceholder.typicode.com';

  Future<List<Post>> fetchAllPosts() async {
    var reqUri = Uri.https(baseUri, '/posts');

    var res = await http.get(reqUri);

    if (res.statusCode == 200) {
      return (convert.jsonDecode(res.body) as List<dynamic>)
          .map((e) => Post.fromJson(e))
          .toList();
    } else {
      // 알맞은 statusCode 처리
      return [];
    }
  }

}
  • convert패키지, jsonDecode함수를 사용하여 String 형태의 json객체를 Map<String, dynamic> 형태로 디코딩한다.
  • map구문을 사용하여 리스트 객체안의 모든 json object를 Post model로 매핑한다.
  • toList()로 List 형태의 데이터로 가공한다.


❗ Dart에서 비동기 처리의 결과 데이터는 Future 객체로 관리된다. http프로토콜로 요청되는 모든 함수(GET/POST/DELETE/PUT등)는 Future객체를 반환한다.

Future객체에서 정상처리 후 리턴시 호출되는 then구문을 사용하여 다음과 같이 나타낼 수 있다.

   List<Post> success({required http.Response res}) {
    if (res.statusCode == 200) {
      return (convert.jsonDecode(res.body) as List<dynamic>)
          .map((e) => Post.fromJson(e))
          .toList();
    } else {
      return [];
    }
  }

  Future<List<Post>> fetchAllPosts() async {
    var reqUri = Uri.https(baseUri, '/posts');

    return await http.get(reqUri).then((res) => success(res: res)).onError(
        (error, stackTrace) => throw Exception('error fetchAllPosts'));
  }


추가로, 특정 위치(start)와 페이지 제한(limit)을 지정해서 리스트를 얻어오는 함수, 특정 Post id를 지정하여 해당 Post를 가져오는 함수를 만들어보자.

  Future<List<Post>> fetchPosts({int? start = 0, int? limit = 10}) async {
    var reqUri = Uri.https(baseUri, '/posts',
        <String, String>{'_start': '$start', '_limit': '$limit'});

    return await http.get(reqUri).then((res) => success(res: res)).onError(
        (error, stackTrace) => throw Exception('error fetchAllPosts4Dynamic'));
  }

  Future<Post> getPost({required int id}) async {
    var reqUri = Uri.https(baseUri, '/posts/$id');

    var res = await http.get(reqUri);

    if (res.statusCode == 200) {
      return Post.fromJson(convert.jsonDecode(res.body));
    } else {
      throw Exception('error fetchAllPosts4Dynamic');
    }
  }

모든 작업이 끝났다. API관리객체가 정상적으로 작동하는지 확인하기 위해 test패키지를 사용하여 유닛테스트를 진행해보자.

👀 Dart Unit Test

Dart test 파일은 프로젝트 root/test에 위치시키도록 한다.

📃 test/fake_post_api_test.dart

// ignore_for_file: unnecessary_type_check

import 'package:exam_http/api/fake_post_api.dart';
import 'package:exam_http/model/post.dart';
import 'package:test/test.dart';

void main() async {
  FakePostApi api = FakePostApi();

  group('Post', () {
    group('list', () {
      test('isNotEmpty', () {
        Future<List<Post>> future = api.fetchAllPosts();
        future.then((value) {
          expect(value.length, 100);
        });
      });

      test('get lists from the defined offset', () async {
        Future<List<Post>> future = api.fetchPosts(start: 0, limit: 10);
        future.then((value) {
          expect(value.length, 10);
        });
      });
    });

    group('a Post', () {
      test('isNotNull', () async {
        Future<Post> future = api.getPost(id: 1);
        future.then((value) {
          expect(value.id, 1);
          expect(value.title.length, greaterThan(10));
        });
      });
    });
  });
}
  • group을 사용하여 테스트 그룹을 카테고리별로 관리할 수 있다.
  • test는 하나의 테스트 작업(Task)을 말한다. 테스트 안에는 n개의 테스트(expect, assert)가 있을 수 있다.
  • 테스트에 사용된 함수는 Future객체를 반환하므로 Future객체를 선언하고 결과를 받는다. 이때, 받는 데이터의 형태를 지정하여 야만 expect절에서 해당 데이터 속성을 접근할 수 있다.

1. 커맨드로 유닛테스트 실행

$ flutter test --reporter=expanded
00:00 +0: Post list isNotEmpty
00:00 +1: Post list get lists from the given param
00:00 +2: Post a Post isNotNull
00:00 +3: All tests passed!

2. Visual Studio Code에서 유닛테스트 실행

 

마치며..

http 패키지를 사용하여 API서버와 통신하는 객체를 만들고 테스트까지 진행해보았다.  다음 블로그에서는 여기서 만든 API 객체를 사용하여 Flutter ListView를 출력하는 예제를 살펴본다. 

반응형