Testing Flutter Apps | Flutter
mockito | Dart Package
Flutter ์ฑ ํ ์คํธ ์ ํ ๋ฐ ๋ฐฉ๋ฒ ์ดํด๋ณด๊ธฐ
์ด์ ๊ธ์์๋ Flutter์์ ์ฑ ํ ์คํ ์ ํ๋ ๋ฐฉ๋ฒ ๋ฐ ํ ์คํธ ์ ํ์ ๋ํด์ ๊ธฐ๋ณธ์ ์ธ ๋ด์ฉ์ ๋ค๋ค๋ดค๋๋ฐ, ์ด๋ฒ ๊ธ์์๋ ๋จ์ํ ์คํธ์ ๋ํ ์ข ๋ ์ค์ฉ์ ์ธ ๋ถ๋ถ์ ์ ์ฉํ์ฌ ์์ธํ ๋ด์ฉ์ ๋ค๋ค๋ณด๋ ค๊ณ ํ๋ค.
์ค์ ๋ก ์ด์๋๋ ๋๋ถ๋ถ์ ์๋น์ค๋ API ํต์ ์ ์ฌ์ฉํด ์ํธ์์ฉ ํ๋๋ก ๊ฐ๋ฐ์ด ๋์ด ์๊ธฐ ๋๋ฌธ์ ์ํํ ๋จ์ํ ์คํธ๋ฅผ ์งํํ๋ ค๋ฉด ํ ์คํธ ๋จ๊ณ์์๋ API ํต์ ๋ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ก ๋ถํฐ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ ๊ฒ์ด๋ค.
๋ง์ผ, ์ด ๋ถ๋ถ์ ์ ์ธํ๊ณ ๋จ์ํ ์คํธ๊ฐ ์งํ์ด ๋๋ฉด ์ฌ์ค ๋จ์ํ ์คํธ๋ฅผ ํ๋ ์๋ฏธ๊ฐ ์์๋ฟ๋๋ฌ ์ ํํ ๋จ์ํ ์คํธ ์งํ์ด ์๋๊ฒ ๋๋ค.
์ง๊ธ๋ถํฐ API ํต์ ์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฑ์ ์ธ๋ถ ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํด ํ ์คํธ๋ฅผ ์งํํ๋ ๋ฐฉ๋ฒ์ ๋ํด์ ์ดํด๋ณด๋๋ก ํ์.
์ค์ ๊ตฌ๋๋๋ ์๋น์ค์์ API ํต์ , ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉ ๋ฑ์ ๊ฑฐ์ ๋๋ถ๋ถ์ ํ์ ์์์ผ ๊ฒ์ด๋ค.
API ํต์ ๊ณผ ๊ฐ์ ์ธ๋ถ ์์กด์ฑ์ ํ ์คํธํ๊ธฐ ์ํด Flutter์์ ์ฌ์ฉํ๋ ํจํค์ง๊ฐ ๋ฐ๋ก mockito์ด๋ค.
ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ์ง ์์๋ค๋ฉด ์์ํ ํจํค์ง์ผ ๊ฒ์ด๋ค.
mockito๋ ๋ชจ์ ๊ฐ์ฒด(Mock Object)๋ฅผ ์ฌ์ฉํ์ฌ ์์กด์ฑ ์ฃผ์ ๊ณผ ์ธ๋ถ ๋ฆฌ์์ค ํธ์ถ์ ์๋ฎฌ๋ ์ด์ ํจ์ผ๋ก์จ ํ ์คํธ๋ฅผ ์ฝ๊ฒ ์์ฑํ ์ ์๊ฒ ํด์ฃผ๋ ํจํค์ง๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
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
๋จผ์ 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๋ฅผ ์ฌ์ฉํ ์์ ๋ฅผ ๋ง๋ค์ด ๊ธ์ ์ถ๊ฐ๋ก ์์ฑํ๋๋ก ํ๊ฒ ๋ค.