개요
Flutter에서 http 패키지를 사용하여 파일을 업로드해야할 때가 있습니다. 이때, http 패키지의 MultipartRequest
를 사용하면 파일을 업로드할 수 있습니다. 이번 블로그 포스트에서는 MultipartRequest
를 사용하여 파일을 업로드하는 방법과 이를 테스트하는 방법에 대해서 알아보겠습니다.
MultipartRequest로 파일 업로드하기
다음은 실제로 http 패키지의 MultipartRequest
를 사용하여 파일을 업로드하는 예제입니다.
class ExampleAPI {
final String token;
final http.MultipartRequest httpClient;
ExampleAPI({
required this.token,
@visibleForTesting http.MultipartRequest? mockClient,
}) : httpClient = mockClient ??
http.MultipartRequest(
"POST",
Uri.parse('${ENV.apiServer}/api/app'),
);
Future<InfoData> sendData({
required DateTime date,
required int status,
required int docType,
required String note,
required String passportImage,
String? certificationImage,
List<String>? receiptImages,
}) async {
Map<String, String> data = {};
data['date'] = dateFormatForSearch(date);
data['status'] = '$status';
data['doc_type'] = '$docType';
data['note'] = note;
List<MultipartFile> files = [];
files.add(await http.MultipartFile.fromPath('passport_image', passportImage));
if (jpnCertImg != null && jpnCertImg != '') {
files.add(await http.MultipartFile.fromPath('cert_img', certificationImage));
}
if (receiptImages?.isNotEmpty == true) {
for (var image in receiptImages!) {
if (image == '') continue;
files.add(await http.MultipartFile.fromPath('receipt_img[]', image));
}
}
httpClient.headers.addAll({'Authorization': 'Bearer $token'});
httpClient.fields.addAll(data);
httpClient.files.addAll(files);
final stream = await httpClient.send();
return http.Response.fromStream(stream).then((response) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
if (data['success'] == true) {
return InfoData.fromJson(data['data']);
} else {
throw Exception('Unknown response');
}
});
}
}
http 패키지의 MultipartRequest
를 사용하여 파일을 업로드하는 부분을 좀 더 자세히 살펴보겠습니다.
ExampleAPI({
required this.token,
@visibleForTesting http.MultipartRequest? mockClient,
}) : httpClient = mockClient ??
http.MultipartRequest(
"POST",
Uri.parse('${ENV.apiServer}/api/app'),
);
MultipartRequst
로 파일을 전송하려면 우선, http.MultipartRequest
의 인스턴스를 생성해야 합니다.
Map<String, String> data = {};
data['date'] = dateFormatForSearch(date);
data['status'] = '$status';
data['doc_type'] = '$docType';
data['note'] = note;
MultipartRequest
는 파일 이외에 다른 정보도 함께 보낼 수 있습니다. 그래서 함께 보낼 다른 정보를 준비하였습니다.
List<MultipartFile> files = [];
files.add(await http.MultipartFile.fromPath('passport_image', passportImage));
if (jpnCertImg != null && jpnCertImg != '') {
files.add(await http.MultipartFile.fromPath('cert_img', certificationImage));
}
if (receiptImages?.isNotEmpty == true) {
for (var image in receiptImages!) {
if (image == '') continue;
files.add(await http.MultipartFile.fromPath('receipt_img[]', image));
}
}
파일을 설정하는 함수는 업로드하고자 하는 파일의 경로를 전달받습니다. 이렇게 전달받은 파일의 경로를 http.MultipartFile.fromPath
를 사용하여 MultipartFile
로 변환합니다.
여러 파일을 업로드할 때에는 receipt_img[]
와 같이 []
를 사용하여 배열로 전달할 수 있습니다.
httpClient.headers.addAll({'Authorization': 'Bearer $token'});
httpClient.fields.addAll(data);
httpClient.files.addAll(files);
이제 파일을 업로드하기 위해 Bearer
토큰을 헤더에 설정하고, 준비한 데이터를 fields
에 추가합니다. 준비한 파일은 files
에 추가합니다.
final stream = await httpClient.send();
return http.Response.fromStream(stream).then((response) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
if (data['success'] == true) {
return InfoData.fromJson(data['data']);
} else {
throw Exception('Unknown response');
}
});
마지막으로 데이터를 전송하고 응답을 받습니다. 이렇게 받은 응답을 자신에 앱에 맞게 처리하면 됩니다.
테스트 코드
http
패키지의 MultipartRequest
를 사용하여 파일을 업로드하는 함수를 테스트하는 방법에 대해서 알아보겠습니다.
우선 전체 코드는 다음과 같습니다.
...
@GenerateMocks([http.MultipartRequest])
void main() {
final mockHttpMultipartRequest = CustomMockMultipartRequest();
setUp(() {
mockHttpMultipartRequest.headers.clear();
mockHttpMultipartRequest.fields.clear();
mockHttpMultipartRequest.files.clear();
when(mockHttpMultipartRequest.send()).thenAnswer(
(_) async {
final responseBody = jsonEncode({
'success': true,
'data': {
'amount': 1000,
'commission': 3000,
}
});
final stream = Stream.value(utf8.encode(responseBody));
return http.StreamedResponse(stream, 200);
},
);
});
test('Success', () async {
final result = await TaxRefundAPI(
token: 'test_token',
client: mockHttpMultipartRequest,
).sendData(
permitDate: DateTime.parse('2022-01-01 01:04'),
status: 91,
docType: 2,
note: '',
passportImg: 'assets/images/passport_sample.jpg',
certificationImage: 'assets/images/placeholder.png',
receiptImages: [
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
],
);
// Request parameters
expect(
mockHttpMultipartRequest.headers,
{'Authorization': 'Bearer test_token'},
);
expect(
mockHttpMultipartRequest.fields,
{
'date': '2022-01-01',
'status': '91',
'doc_type': '2',
'note': '',
},
);
expect(mockHttpMultipartRequest.files.length, 5);
expect(mockHttpMultipartRequest.files[0].field, 'passport_image');
expect(mockHttpMultipartRequest.files[0].filename, 'passport_sample.jpg');
expect(mockHttpMultipartRequest.files[1].field, 'cert_img');
expect(mockHttpMultipartRequest.files[1].filename, 'placeholder.png');
expect(mockHttpMultipartRequest.files[2].field, 'receipt_img[]');
expect(
mockHttpMultipartRequest.files[2].filename,
'sample_receipt.png',
);
expect(mockHttpMultipartRequest.files[3].field, 'receipt_img[]');
expect(
mockHttpMultipartRequest.files[3].filename,
'sample_receipt.png',
);
expect(mockHttpMultipartRequest.files[4].field, 'receipt_img[]');
expect(
mockHttpMultipartRequest.files[4].filename,
'sample_receipt.png',
);
// Response
expect(result, isA<InfoData>());
expect(result.toMap(), {
'amount': 1000,
'commission': 3000,
});
});
test('Throw error when response is failed', () async {
when(mockHttpMultipartRequest.send()).thenAnswer(
(_) async {
final responseBody = jsonEncode({
'success': false,
});
final stream = Stream.value(utf8.encode(responseBody));
return http.StreamedResponse(stream, 200);
},
);
try {
await TaxRefundAPI(
token: 'test_token',
client: mockHttpMultipartRequest,
).sendData(
permitDate: DateTime.parse('2022-01-01 01:04'),
status: 91,
docType: 2,
note: '',
passportImg: 'assets/images/passport_sample.jpg',
certificationImage: 'assets/images/placeholder.png',
receiptImages: [
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
],
);
} catch (e) {
expect(e.toString(), 'Exception: Unknown response');
}
});
}
class CustomMockMultipartRequest extends MockMultipartRequest {
@override
final Map<String, String> headers = {};
@override
final Map<String, String> fields = {};
@override
final List<MultipartFile> files = [];
}
좀 더 자세히 살펴보도록 하겠습니다.
...
@GenerateMocks([http.MultipartRequest])
void main() {
final mockHttpMultipartRequest = CustomMockMultipartRequest();
setUp(() {
mockHttpMultipartRequest.headers.clear();
mockHttpMultipartRequest.fields.clear();
mockHttpMultipartRequest.files.clear();
when(mockHttpMultipartRequest.send()).thenAnswer(
(_) async {
final responseBody = jsonEncode({
'success': true,
'data': {
'amount': 1000,
'commission': 3000,
}
});
final stream = Stream.value(utf8.encode(responseBody));
return http.StreamedResponse(stream, 200);
},
);
});
...
}
class CustomMockMultipartRequest extends MockMultipartRequest {
@override
final Map<String, String> headers = {};
@override
final Map<String, String> fields = {};
@override
final List<MultipartFile> files = [];
}
DI(Dependency Injection)
를 사용하고 있기 때문에, 테스트를 위한 Mock
객체를 생성하고, setUp
함수를 사용하여 Mock
객체를 초기화했습니다.
...
@GenerateMocks([http.MultipartRequest])
void main() {
final mockHttpMultipartRequest = CustomMockMultipartRequest();
...
test('Success', () async {
final result = await TaxRefundAPI(
token: 'test_token',
client: mockHttpMultipartRequest,
).sendData(
permitDate: DateTime.parse('2022-01-01 01:04'),
status: 91,
docType: 2,
note: '',
passportImg: 'assets/images/passport_sample.jpg',
certificationImage: 'assets/images/placeholder.png',
receiptImages: [
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
],
);
// Request parameters
expect(
mockHttpMultipartRequest.headers,
{'Authorization': 'Bearer test_token'},
);
expect(
mockHttpMultipartRequest.fields,
{
'date': '2022-01-01',
'status': '91',
'doc_type': '2',
'note': '',
},
);
expect(mockHttpMultipartRequest.files.length, 5);
expect(mockHttpMultipartRequest.files[0].field, 'passport_image');
expect(mockHttpMultipartRequest.files[0].filename, 'passport_sample.jpg');
expect(mockHttpMultipartRequest.files[1].field, 'cert_img');
expect(mockHttpMultipartRequest.files[1].filename, 'placeholder.png');
expect(mockHttpMultipartRequest.files[2].field, 'receipt_img[]');
expect(
mockHttpMultipartRequest.files[2].filename,
'sample_receipt.png',
);
expect(mockHttpMultipartRequest.files[3].field, 'receipt_img[]');
expect(
mockHttpMultipartRequest.files[3].filename,
'sample_receipt.png',
);
expect(mockHttpMultipartRequest.files[4].field, 'receipt_img[]');
expect(
mockHttpMultipartRequest.files[4].filename,
'sample_receipt.png',
);
// Response
expect(result, isA<InfoData>());
expect(result.toMap(), {
'amount': 1000,
'commission': 3000,
});
});
...
}
...
앞서 생선한 Mock
객체를 사용하여 보낸 요청이 성공적으로 처리된 경우를 테스트하였습니다.
...
@GenerateMocks([http.MultipartRequest])
void main() {
final mockHttpMultipartRequest = CustomMockMultipartRequest();
...
test('Throw error when response is failed', () async {
when(mockHttpMultipartRequest.send()).thenAnswer(
(_) async {
final responseBody = jsonEncode({
'success': false,
});
final stream = Stream.value(utf8.encode(responseBody));
return http.StreamedResponse(stream, 200);
},
);
try {
await TaxRefundAPI(
token: 'test_token',
client: mockHttpMultipartRequest,
).sendData(
permitDate: DateTime.parse('2022-01-01 01:04'),
status: 91,
docType: 2,
note: '',
passportImg: 'assets/images/passport_sample.jpg',
certificationImage: 'assets/images/placeholder.png',
receiptImages: [
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
'assets/images/sample_receipt.png',
],
);
} catch (e) {
expect(e.toString(), 'Exception: Unknown response');
}
});
}
...
마지막으로, 응답이 실패한 경우를 테스트하였습니다.
완료
이것으로 Flutter에서 http 패키지의 MultipartRequest
를 사용하여 파일을 업로드하는 방법과 이를 테스트하는 방법에 대해서 알아보았습니다.
Flutter에서 파일 업로드 기능을 구현하신다면, 이 블로그를 참고하여 구현하고, 테스트 코드를 작성해 보시기 바랍니다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku
가 개발한 앱을 한번 사용해보세요.Deku
가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.