๐Ÿง€ [Flutter] API ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ mockito ์‚ฌ์šฉํ•ด ๋ณด๊ธฐ

Tygerยท2024๋…„ 6์›” 15์ผ
1

Flutter

๋ชฉ๋ก ๋ณด๊ธฐ
62/64

๐Ÿง€ API ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ mockito ์‚ฌ์šฉ ํ•ด๋ณด๊ธฐ

Testing Flutter Apps | Flutter
mockito | Dart Package

Flutter ์•ฑ ํ…Œ์ŠคํŠธ ์œ ํ˜• ๋ฐ ๋ฐฉ๋ฒ• ์‚ดํŽด๋ณด๊ธฐ

์ด์ „ ๊ธ€์—์„œ๋Š” Flutter์—์„œ ์•ฑ ํ…Œ์ŠคํŒ…์„ ํ•˜๋Š” ๋ฐฉ๋ฒ• ๋ฐ ํ…Œ์ŠคํŠธ ์œ ํ˜•์— ๋Œ€ํ•ด์„œ ๊ธฐ๋ณธ์ ์ธ ๋‚ด์šฉ์„ ๋‹ค๋ค„๋ดค๋Š”๋ฐ, ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋‹จ์œ„ํ…Œ์ŠคํŠธ์— ๋Œ€ํ•œ ์ข€ ๋” ์‹ค์šฉ์ ์ธ ๋ถ€๋ถ„์„ ์ ์šฉํ•˜์—ฌ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ๋‹ค๋ค„๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

์‹ค์ œ๋กœ ์šด์˜๋˜๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์„œ๋น„์Šค๋Š” API ํ†ต์‹ ์„ ์‚ฌ์šฉํ•ด ์ƒํ˜ธ์ž‘์šฉ ํ•˜๋„๋ก ๊ฐœ๋ฐœ์ด ๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์›ํ™œํ•œ ๋‹จ์œ„ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ ค๋ฉด ํ…Œ์ŠคํŠธ ๋‹จ๊ณ„์—์„œ๋„ API ํ†ต์‹  ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•  ๊ฒƒ์ด๋‹ค.

๋งŒ์ผ, ์ด ๋ถ€๋ถ„์„ ์ œ์™ธํ•˜๊ณ  ๋‹จ์œ„ํ…Œ์ŠคํŠธ๊ฐ€ ์ง„ํ–‰์ด ๋˜๋ฉด ์‚ฌ์‹ค ๋‹จ์œ„ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ์˜๋ฏธ๊ฐ€ ์—†์„๋ฟ๋”๋Ÿฌ ์ •ํ™•ํ•œ ๋‹จ์œ„ํ…Œ์ŠคํŠธ ์ง„ํ–‰์ด ์•ˆ๋˜๊ฒŒ ๋œ๋‹ค.

์ง€๊ธˆ๋ถ€ํ„ฐ API ํ†ต์‹ ์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ์˜ ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

mockito

์‹ค์ œ ๊ตฌ๋™๋˜๋Š” ์„œ๋น„์Šค์—์„œ API ํ†ต์‹ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ ๋“ฑ์€ ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„์˜ ํ•„์ˆ˜ ์š”์†Œ์ผ ๊ฒƒ์ด๋‹ค.

API ํ†ต์‹ ๊ณผ ๊ฐ™์€ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด Flutter์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ‚ค์ง€๊ฐ€ ๋ฐ”๋กœ mockito์ด๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์ƒ์†Œํ•œ ํŒจํ‚ค์ง€์ผ ๊ฒƒ์ด๋‹ค.

mockito๋Š” ๋ชจ์˜ ๊ฐ์ฒด(Mock Object)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜์กด์„ฑ ์ฃผ์ž…๊ณผ ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค ํ˜ธ์ถœ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•จ์œผ๋กœ์จ ํ…Œ์ŠคํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํŒจํ‚ค์ง€๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

dependencies

mockito ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ์ฝ”๋“œ ์ž๋™์ƒ์„ฑ์„ ์œ„ํ•œ build_runner๋„ ์ถ”๊ฐ€ํ•ด์ฃผ์ž.

API ๋‹จ์œ„ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด http๋„ ์ถ”๊ฐ€ํ•ด ์ฃผ์ž.

dependencies:
  http: ^1.2.0

dev_dependencies:
  build_runner: ^2.4.9
  flutter_test:
    sdk: flutter
  mockito: ^5.4.4

Helper

๋จผ์ € mockito ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๊ธฐ ์œ„ํ•ด helper_test.dart ํŒŒ์ผ์„ test ํด๋” ์•„๋ž˜์— ์ƒ์„ฑํ•ด ์ฃผ๋„๋ก ํ•˜์ž.

@GenerateMocks ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด http ํŒจํ‚ค์ง€์˜ Client ๊ฐ์ฒด์— ๋Œ€ํ•œ ๋ชจ์˜ ๊ฐ์ฒด ์ฝ”๋“œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ์‹œ์ผœ ์ฃผ์ž.

helper_test.dart

import 'package:mockito/annotations.dart';
import 'package:http/http.dart' as http;

(
  [],
  customMocks: [MockSpec<http.Client>(as: #MockHttpClient)],
)
void main() {}

์ด์ œ Code generator๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”๋ฐ, ๋ช…๋ น์–ด๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

build_runner ํŒจํ‚ค์ง€๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ์žˆ์–ด์•ผ๋งŒ ์ƒ์„ฑ๊ธฐ์— ์˜ํ•ด ์ž๋™์œผ๋กœ ์ฝ”๋“œ๊ฐ€ ์ž‘์„ฑ๋˜๋‹ˆ ํŒจํ‚ค์ง€ ์ถ”๊ฐ€ ์—ฌ๋ถ€๋ฅผ ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•˜์—ฌ์•ผ ํ•œ๋‹ค.

flutter pub run build_runner build

Code generator๋ฅผ ๊ฐœ๋ฐœ ์ค‘์— ๊ณ„์† ์‹คํ–‰ํ•˜๋ฉด์„œ, ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ์„์‹œ ์ž๋™์œผ๋กœ build_runner๊ฐ€ ์‹คํ–‰๋˜๊ธธ ์›ํ•œ๋‹ค๋ฉด ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

flutter pub run build_runner watch --delete-conflicting-outputs

์ •์ƒ์ ์œผ๋กœ ์ž๋™ ์ƒ์„ฑ๊ธฐ์— ์˜ํ•ด test_helper.mocks.dart ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์—ˆ์„ ๊ฒƒ์ด๋‹ค.

์œ„์— helper ์ฝ”๋“œ์˜ ์–ด๋…ธํ…Œ์ด์…˜ ๋ถ€๋ถ„์— ๋Œ€ํ•ด ์ถ”๊ฐ€์ ์ธ ์„ค๋ช…์„ ํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

mockito ํŒจํ‚ค์ง€๋Š” ๋ชจ์˜ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๊ธฐ ๋•Œ๋ฌธ์—, ๋ชจ์˜ ๊ฐ์ฒด๋ฅผ ์ฝ”๋“œ ์ œ๋„ˆ๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด ์ƒ์„ฑํ•ด ์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

์—ฌ๊ธฐ์„œ ๋ชจ์˜ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ์ค€์ด ๋ฐ”๋กœ ์–ด๋…ธํ…Œ์ด์…˜์— ์ถ”๊ฐ€๋œ ๊ฐ์ฒด์ด๋‹ค.

(
  [
  	// ๋ชจ์˜ ๊ฐ์ฒด
  ],
)

customMocks ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ๋ชจ์˜ ๊ฐ์ฒด๊ฐ€ ์ž๋™ ์ƒ์„ฑ๋˜์–ด ์งˆ ๋•Œ์— ํด๋ž˜์Šค ๋ช…์„ ์›ํ•˜๋Š” ์ด๋ฆ„์œผ๋กœ ๋ช…๋ช…ํ•˜๊ณ ์ž ํ•  ๋•Œ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—, http ํŒจํ‚ค์ง€์˜ Client ๊ฐ์ฒด์˜ ๋ชจ์˜๊ฐ์ฒด์˜ ์ด๋ฆ„์€ ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ์ง€์ •ํ•œ MockHttpClient๋กœ ์ƒ์„ฑ๋˜๋„๋ก ํ•˜๋ ค๊ณ  customMocks์—์„œ ์ƒ์„ฑํ•˜์˜€๋‹ค.

(
  [],
  customMocks: [MockSpec<http.Client>(as: #MockHttpClient)],
)

์ด๋ ‡๊ฒŒ ์ƒ์„ฑํ–ˆ๋‹ค๋ฉด, ๋ชจ์˜ ๊ฐ์ฒด ์ด๋ฆ„์€ MockClient๊ฐ€ ๋œ๋‹ค.

(
  [
  	http.Client,
  ],
)

์ด ๋ถ€๋ถ„์€ ์ถ”๊ฐ€์ ์ธ ๋ชจ์˜ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด ๋ณด๋ฉด์„œ ๋” ์‚ดํŽด๋ณด๋„๋ก ํ•˜์ž.

API์— ์‚ฌ์šฉํ•  ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์ž.

class Urls {
  static const String base = "https://picsum.photos";
  static String currentImageByNo(int no) => "$base/id/$no/info";
}

์ด์ œ API ํ†ต์‹ ์‹œ ์„ฑ๊ณต, ์‹คํŒจ์— ๋Œ€ํ•œ ์ผ€์ด์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๋„๋ก ํ•˜์ž.

test ํด๋” ์•ˆ์— test ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์„œ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์ž.

void main() {
  late MockHttpClient mockHttpClient;
  late ImageRepository imageRepository;

  setUp(() {
    mockHttpClient = MockHttpClient();
    imageRepository = ImageRepository(mockHttpClient);
  });
}

setUp ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ์ธ์Šคํ„ด์Šค๋ฅผ ์ดˆ๊ธฐํ™” ํ•ด์ฃผ์ž.

์šฐ๋ฆฌ๋Š” ์•„์ง image๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ฝ”๋“œ๊ฐ€ ๊ตฌํ˜„๋˜์–ด ์žˆ์ง€ ์•Š์œผ๋‹ˆ, ImageRepository ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ์ž.

class ImageRepository {
  final http.Client client;

  const ImageRepository(this.client);

  Future<void> fetch(int no) async {
    try {
      await client.get(Uri.parse(Urls.currentImageByNo(no)));
    } catch (_) {}
  }
}

๋‹ค์‹œ ํ…Œ์ŠคํŠธ ํŒŒ์ผ๋กœ ์™€์„œ ์„ฑ๊ณต์‹œ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ์ž.

when ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ๋ชจ์˜ ๊ฐ์ฒด์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰์‹œ์ผœ ์ค€ ๋’ค, thenAnswer๋ฅผ ์‚ฌ์šฉํ•ด ์‘๋‹ต์ด 200์ธ ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์™€ ์ฃผ๋„๋ก ํ•˜์ž.

verifyNever๋Š” API๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•˜์€ ์ƒํƒœ์ธ์ง€์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ์ด๋‹ค.

verify๋Š” ๋ช‡ ๋ฒˆ ํ˜ธ์ถœ๋œ ์ƒํƒœ์ธ์ง€๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ์— ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

void main() {
	...
    const int testId = 3;

  test("fetch completes successfully when the HTTP call returns 200", () async {
    when(mockHttpClient.get(Uri.parse(Urls.currentImageByNo(testId))))
        .thenAnswer((_) async => http.Response("", 200));

    verifyNever(mockHttpClient.get(Uri.parse(Urls.currentImageByNo(testId))));

    await imageRepository.fetch(testId);

    verify(mockHttpClient.get(Uri.parse(Urls.currentImageByNo(testId))));
  });
}

imageRepository.fetch(testId)๋ฅผ ์‚ฌ์šฉํ•ด API๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์ „์ด๊ธฐ์— verifyNever ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๊ณ , ํ˜ธ์ถœ ํ›„ verify ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ฒŒ ๋œ๋‹ค.

verify๋Š” called๋ฅผ ์‚ฌ์šฉํ•ด ๋ช‡ ๋ฒˆ ํ˜ธ์ถœ๋œ ์ƒํƒœ์ธ์ง€๋ฅผ ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๋‹ค.

verify(mockHttpClient.get(Uri.parse(Urls.currentImageByNo(testId))))
        .called(3);

์ด์–ด์„œ ์—๋Ÿฌ ์ผ€์ด์Šค์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋„ ์ง„ํ–‰ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค.

 test('fetch handles errors when the HTTP call returns non-200 status code',
      () async {
    when(mockHttpClient.get(Uri.parse(Urls.currentImageByNo(testId))))
        .thenAnswer((_) async => http.Response("Not Found", 404));

    await imageRepository.fetch(testId);

    verify(mockHttpClient.get(Uri.parse(Urls.currentImageByNo(testId))));
  });

http ํŒจํ‚ค์ง€๊ฐ€ ์•„๋‹Œ dio ํŒจํ‚ค์ง€ ์‚ฌ์šฉ์‹œ helper ์–ด๋…ธํ…Œ์ด์…˜์— dio ๊ฐ์ฒด๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์€ ๋™์ผํ•ด์ง„๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์ด๋ฒˆ ๊ธ€์„ ์ž‘์„ฑํ•˜๋ฉด์„œ mockito์— ๋Œ€ํ•œ ๋‚ด์šฉ๊ณผ TDD ๊ตฌ์กฐ๋ฅผ ๊ฒฐํ•ฉํ•ด์„œ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•ด ์„ค๋ช…ํ•˜๋ ค๊ณ  ํ•˜์˜€๋Š”๋ฐ, ์•„ํ‚คํ…์ณ ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด, ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ์™€ ํŒŒ์ผ์ด ๋งŽ์•„์ ธ ๊ธ€๋กœ ์„ค๋ช…ํ•˜๋Š”๋ฐ ํ•œ๊ณ„๊ฐ€ ์žˆ์–ด ๊ฐ€๋ณ๊ฒŒ๋งŒ ์ž‘์„ฑํ•˜์˜€๋‹ค.

API๋ฅผ ํ…Œ์ŠคํŠธ ํ•  ๋•Œ์—๋Š” ๋‹จ์ˆœํžˆ ํ˜ธ์ถœ ์„ฑ๊ณต, ์‹คํŒจ ์™ธ์—๋„ ๋ฐ์ดํ„ฐ์˜ ๊ตฌ์กฐ๊ฐ€ ๋™์ผํ•œ์ง€ json ํŒŒ์‹ฑ์ด ์ •์ƒ์ ์ธ์ง€ ๋“ฑ์˜ ์—ฌ๋Ÿฌ ์ผ€์ด์Šค๊ฐ€ ์žˆ๋Š”๋ฐ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋˜ ํ•จ์ˆ˜์™€ ํฌ๊ฒŒ ๋‹ค๋ฅด์ง€ ์•Š์œผ๋‹ˆ ๊ธˆ๋ฐฉ ์ต์ˆ™ํ•ด์งˆ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

๋น ๋ฅธ ์‹œ์ผ๋‚ด์— TDD๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์ œ๋ฅผ ๋งŒ๋“ค์–ด ๊ธ€์„ ์ถ”๊ฐ€๋กœ ์ž‘์„ฑํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

profile
Flutter Developer

0๊ฐœ์˜ ๋Œ“๊ธ€