4-qism · Sifat va ishlab chiqarish·11-bob
Testing — unit, widget, integration va golden testlar
Flutter/Dart test piramidasi to'liq qamrovda: unit va mocking'dan tortib widget testlari, integration test va golden testlargacha — CI pipeline'ga qanday joylashtirish bilan birga.
Senior lavozim uchun intervyuda testlash mavzusi ko'pincha "qanday test yozasiz" emas, balki "qaysi turdagi testni qachon yozasiz va nega" shaklida so'raladi. Bu bobda Flutter/Dart test piramidasining barcha qatlamlari va ularni to'g'ri joyda qo'llash mantig'i yoritiladi.
1. Test piramidasi — nega bir xil emas#
Test piramidasi — testlarni tezlik va ishonchlilik nisbatiga qarab qatlamlashtirish g'oyasi:
Piramidaning pastki qismi ko'proq, yuqori qismi kamroq bo'lishi kerak — sabab: unit test sekundning bir necha ulushida ishlaydi va aniq nima buzilganini ko'rsatadi, integration test esa daqiqalab ishlashi mumkin va muvaffaqiyatsiz bo'lganda sabab noaniqroq bo'ladi (UI'da qayerdadir biror narsa noto'g'ri).
2. Unit testlar — test paketi#
Unit test — bitta funksiya yoki klassni, tashqi bog'liqliklardan (tarmoq, baza, fayl tizimi) ajratilgan holda tekshiradi.
// lib/price_calculator.dart
class PriceCalculator {
double applyDiscount(double price, double percent) {
if (percent < 0 || percent > 100) {
throw ArgumentError('Chegirma foizi 0-100 oralig\'ida bo\'lishi kerak');
}
return price - (price * percent / 100);
}
}
// test/price_calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/price_calculator.dart';
void main() {
late PriceCalculator calculator;
setUp(() {
calculator = PriceCalculator();
});
group('applyDiscount', () {
test('20% chegirmani to\'g\'ri hisoblaydi', () {
expect(calculator.applyDiscount(100, 20), equals(80));
});
test('0% chegirmada narx o\'zgarmaydi', () {
expect(calculator.applyDiscount(100, 0), equals(100));
});
test('noto\'g\'ri foiz uchun xato tashlaydi', () {
expect(() => calculator.applyDiscount(100, 150), throwsArgumentError);
});
});
}setUp/tearDown har bir test oldidan/keyin ishlaydi — testlar bir-biridan
mustaqil bo'lishi uchun umumiy holatni har safar qayta yaratish muhim.
3. Mocking — mockito va mocktail#
Real kodda funksiyalar ko'pincha tashqi bog'liqliklarga (API client, baza) tayanadi. Mock — shu bog'liqlikning "soxta" versiyasi, nazorat qilinadigan xatti-harakat bilan.
// mocktail — kod generatsiyasiz, sof Dart bilan ishlaydi
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
class MockApiClient extends Mock implements ApiClient {}
void main() {
late MockApiClient mockClient;
late UserRepository repository;
setUp(() {
mockClient = MockApiClient();
repository = UserRepository(mockClient);
});
test('foydalanuvchi topilmasa null qaytaradi', () async {
when(() => mockClient.fetchUser(any()))
.thenAnswer((_) async => throw NotFoundException());
final user = await repository.getUser('123');
expect(user, isNull);
});
test('fetchUser aynan bitta marta chaqiriladi', () async {
when(() => mockClient.fetchUser(any()))
.thenAnswer((_) async => User(id: '123', name: 'Ali'));
await repository.getUser('123');
verify(() => mockClient.fetchUser('123')).called(1);
});
}mockito esa kod generatsiyasi (build_runner) orqali ishlaydi — katta
loyihalarda ba'zan afzal ko'riladi, chunki generatsiya vaqtida turdagi
xatolarni ushlab qoladi. mocktail esa generatsiyasiz, tezroq yozish
tajribasi beradi. Ikkalasi ham amalda keng qo'llaniladi.
4. Widget testlar — testWidgets va WidgetTester#
Widget test — Flutter widget'ini haqiqiy qurilmasiz, tezkor simulyatsiya
muhitida (FlutterDriver emas, in-memory binding) ishga tushiradi va uning
UI xatti-harakatini tekshiradi.
testWidgets('tugma bosilganda counter oshadi', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // bitta freymni "qo'lda" ishga tushiradi
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
});Muhim tushunchalar:
pumpWidget— widget daraxtini birinchi marta qurish.pump()— bitta freymni oldinga suradi (masalan,setStatedan keyin UI'ni yangilash uchun).pumpAndSettle()— barcha animatsiyalar va mikrotasklar tugaguncha bir necha freymni ketma-ket suradi (masalan,Navigatoro'tish animatsiyasi tugashini kutish uchun).find.byType/find.byKey/find.text/find.byIcon— widgetlarni turli mezonlar bo'yicha topish.
testWidgets('yuklanish holatida spinner ko\'rinadi', (tester) async {
await tester.pumpWidget(
MaterialApp(home: ProfilePage(repository: FakeSlowRepository())),
);
// pump() emas, pumpAndSettle() emas — hali Future tugamagan, spinner ko'rinishi kerak
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.pumpAndSettle(); // endi Future tugaydi
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('Ali Valiyev'), findsOneWidget);
});5. Integration testlar — integration_test paketi#
Integration test real (yoki emulyator) qurilmada, to'liq ilova bilan ishlaydi — foydalanuvchi oqimining boshidan oxirigacha ishlashini tekshiradi (masalan: "ro'yxatdan o'tish → login → mahsulot qo'shish → checkout").
// integration_test/app_test.dart
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('to\'liq xarid oqimi', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.tap(find.text('Kirish'));
await tester.pumpAndSettle();
await tester.enterText(find.byKey(const Key('email_field')), 'test@aidoc.uz');
await tester.enterText(find.byKey(const Key('password_field')), 'parol123');
await tester.tap(find.text('Tizimga kirish'));
await tester.pumpAndSettle();
expect(find.text('Xush kelibsiz'), findsOneWidget);
});
}Bu testlar sekin (real render, real animatsiyalar, real navigatsiya) va ko'proq resurs talab qiladi — shuning uchun piramidaning tepasida, kamroq sonda saqlanadi. Odatda faqat eng muhim, biznes uchun kritik oqimlar (onboarding, checkout, autentifikatsiya) uchun yoziladi.
6. Golden testlar — vizual regressiya#
Golden test — widgetning render natijasini rasm (.png) sifatida saqlab,
keyingi test yurishlarida piksel-piksel solishtiradi. UI kutilmagan tarzda
o'zgarib qolmaganini (masalan, padding, rang, font o'lchami) ushlaydi.
testWidgets('ProfileCard golden test', (tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(body: ProfileCard(name: 'Ali', role: 'Senior Developer')),
),
);
await expectLater(
find.byType(ProfileCard),
matchesGoldenFile('goldens/profile_card.png'),
);
});Birinchi yurishda flutter test --update-goldens bilan "etalon" rasm
yaratiladi, keyingi yurishlarda joriy render shu rasm bilan solishtiriladi.
7. Testlarni CI pipeline'ga joylashtirish#
# .github/workflows/test.yml
name: Test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage
- run: flutter test integration_test/app_test.dartAmaliy qoida: unit va widget testlar har bir pull request'da ishga
tushirilishi kerak (tez, arzon). Integration testlar ko'pincha faqat
main/release branch'ga birlashtirishdan oldin yoki nightly build'da
ishga tushiriladi, chunki ular sekinroq va ko'proq infratuzilma talab qiladi
(emulyator/real qurilma).
8. Testlanadigan kod yozish — arxitektura ta'siri#
Test yozish qanchalik oson bo'lishi ko'p jihatdan kod arxitekturasiga bog'liq:
- Biznes mantiq UI'dan ajratilgan bo'lishi kerak (masalan,
Bloc/ViewModelqatlamida) — shunda uni widget qurmasdan test qilish mumkin. - Tashqi bog'liqliklar (API, baza, vaqt —
DateTime.now()) interfeys orqali in'ektsiya qilinishi kerak — shunda ularni mock bilan almashtirish mumkin. - Global holat (singleton, static o'zgaruvchilar) testlarni bir-biriga bog'lab qo'yadi — bir test boshqa testning holatiga ta'sir qilishi mumkin.
Ko'p uchraydigan xato javoblar#
| Vaziyat | Junior darajadagi yondashuv | Senior darajadagi yondashuv |
|---|---|---|
| Yangi feature yozildi | "Qo'lda bir marta sinab ko'rdim, ishlayapti" | "Biznes mantiq uchun unit test, UI xatti-harakati uchun widget test yozdim" |
| Test sekin ishlayapti | "Testni o'chirib qo'yaman" | "Bu integration test bo'lishi kerakmi yoki widget test yetarlimi, deb qayta ko'rib chiqaman" |
| Mock kerak | "API'ni to'g'ridan-to'g'ri chaqiraman, internet bor-ku" | "Tarmoqqa bog'liq bo'lmagan, tez va barqaror test uchun mock ishlataman" |
| CI'da test flaky (ba'zan o'tadi, ba'zan yo'q) | "Qayta ishga tushiraman, ba'zan shunday bo'ladi" | "Sabab — odatda pump() o'rniga pumpAndSettle() kerakligi yoki testlar orasida ulashilgan holat; sababini topib tuzataman" |
Tez-tez so'raladigan intervyu savollari#
Savol: Test piramidasi nima va nega uni hurmat qilish kerak?
Javob: Test piramidasi — unit testlarni ko'p va tez, integration testlarni kam va sekin qilib qatlamlash strategiyasi. Sabab: unit test millisekundlarda ishlaydi va aniq qaysi funksiya buzilganini ko'rsatadi, integration test esa sekin va muvaffaqiyatsizlik sababi noaniqroq bo'ladi. Piramidani "teskari" qurish (ko'p integration, kam unit) — sekin, qimmat va disgnostika qilish qiyin test suite'ga olib keladi.
Savol: pump() va pumpAndSettle() orasidagi farq nima va noto'g'ri
ishlatish qanday muammo keltirib chiqaradi?
Javob: pump() bitta freymni oldinga suradi — bitta setState yoki bitta
animatsiya kadrini tekshirish uchun mos. pumpAndSettle() esa barcha
animatsiyalar va navbatdagi mikrotasklar tugaguncha ketma-ket freymlarni
suradi. Agar animatsiya cheksiz davom etsa (masalan, CircularProgressIndicator),
pumpAndSettle() cheksiz kutib, timeout xatosi beradi — shuning uchun
"hali tugamagan" holatni tekshirish uchun aynan pump() ishlatiladi.
Savol: mockito va mocktail orasidagi asosiy farq nima?
Javob: mockito kod generatsiyasi (build_runner + @GenerateMocks)
orqali ishlaydi — bu qo'shimcha build qadamini talab qiladi, lekin ba'zi
murakkab holatlarda kuchliroq turdagi tekshiruv beradi. mocktail esa sof
Dart bilan, generatsiyasiz ishlaydi — yozish tezroq va CI'da qo'shimcha
generatsiya qadami shart emas. Ikkalasi ham when()/verify() uslubidagi
API'ga ega.
Savol: Nega ba'zi jamoalar golden testlarni CI'da faqat bitta platformada (masalan, Linux) ishga tushiradi?
Javob: Shrift renderlash operatsion tizimlar orasida bir oz farq qiladi (subpixel rendering, hinting), shuning uchun bitta mashinada yaratilgan golden rasm boshqa OS'da tabiiy piksel farqi bilan "muvaffaqiyatsiz" bo'lishi mumkin — bu haqiqiy regressiya emas. Yechim: goldenlarni bitta belgilangan, barqaror muhitda (masalan, maxsus Docker image) generatsiya qilish va solishtirish.
Savol: Dependency injection testlash bilan qanday bog'liq?
Javob: Agar klass o'z bog'liqliklarini (API client, baza, vaqt manbai)
ichida new orqali yaratsa, uni test paytida almashtirib bo'lmaydi. Agar
bog'liqliklar konstruktor orqali tashqaridan in'ektsiya qilinsa, test paytida
ularning o'rniga mock/fake obyekt berish mumkin — bu unit testni tezkor va
tashqi muhitdan mustaqil qiladi.
Savol: Widget test va integration test bir xil narsani sinaydimi?
Javob: Yo'q. Widget test in-memory, tezkor render muhitida ishlaydi — haqiqiy platform kanallari, haqiqiy tarmoq, haqiqiy fayl tizimi yo'q (ular avtomatik mock qilinadi). Integration test esa haqiqiy qurilma/emulyatorda, to'liq ilova kontekstida ishlaydi — platform kanallari, haqiqiy navigatsiya animatsiyalari va real performance xususiyatlari bilan. Widget test tezroq va ko'p sonli, integration test sekinroq va faqat kritik oqimlar uchun.
Savol: setUp/tearDown nima uchun kerak va ularsiz nima xato ketishi
mumkin?
Javob: setUp har bir testdan oldin toza holat yaratadi (masalan, yangi
mock instance). Agar buning o'rniga testlar orasida bitta umumiy obyekt
qayta ishlatilsa, bir testda o'zgargan holat keyingi testga "sizib o'tishi"
mumkin — natijada testlar tartibga bog'liq bo'lib qoladi (bitta tartibda
o'tadi, boshqasida yo'q) — bu "flaky test"larning keng tarqalgan sababi.
Savol: Test coverage 100% bo'lishi kerakmi?
Javob: Yo'q, va bu noto'g'ri maqsad. Coverage — qancha qator kod ishga tushganini o'lchaydi, lekin assertsiyalarning sifatini o'lchamaydi — 100% coverage'ga ega, lekin hech narsani tekshirmaydigan testlar yozish mumkin. Senior yondashuv — biznes uchun kritik, murakkab mantiqqa yuqori coverage, oddiy "getter/setter" kabi joylarga past coverage berish.
Savol: find.byKey va find.byType/find.text orasida qachon
qaysinisini tanlaysiz?
Javob: find.text/find.byType matn yoki widget turi o'zgarganda test
ham buzilishi mumkin bo'lgan "qattiq" bog'liqlik yaratadi (masalan, tarjima
matni o'zgarsa). find.byKey esa maxsus, barqaror identifikator bilan
ishlaydi — UI matni yoki tuzilishi o'zgarsa ham test buzilmaydi. Murakkab
yoki bir xil turdagi bir nechta widget bo'lgan ekranlarda Key ishlatish
tavsiya etiladi.
Savol: Nima uchun integration testlar CI'da har bir PR'da emas, balki kamroq tez-tez ishga tushiriladi?
Javob: Ular real emulyator/qurilma ishga tushirishni, to'liq ilova
build qilishni va sekinroq render/animatsiya jarayonlarini talab qiladi —
bir necha daqiqadan o'nlab daqiqagacha vaqt olishi mumkin. Har bir PR'da
ishga tushirish CI xarajatini va developer kutish vaqtini keskin oshiradi,
shuning uchun odatda faqat main/release branch'ga birlashishdan oldin
yoki jadval bo'yicha (nightly) ishga tushiriladi.
Savol: Flaky test (ba'zan o'tadigan, ba'zan yo'q) bilan qanday kurashasiz?
Javob: Avval sababini aniqlayman — odatda: (1) pump() o'rniga vaqtga
bog'liq animatsiya kutilmagan, (2) testlar orasida ulashilgan global holat,
(3) haqiqiy tarmoq/vaqtga bog'liqlik mock qilinmagan. Testni shunchaki qayta
ishga tushirish yoki "retry" qo'shish — sababni yashiradi, hal qilmaydi.
To'g'ri yechim — testni deterministik qiladigan sababni topib tuzatish.
Testlanmagan funksiyaga test yozish
O'z loyihangizdan (yoki yangi kichik loyihada) tashqi bog'liqliksiz bitta sof funksiya toping (masalan, valyuta formatlovchi yoki email validator). Unga kamida 4 ta test holati yozing: to'g'ri kirish, chegaraviy holat (bo'sh qator, 0), noto'g'ri kirish (xato tashlashi kerak bo'lgan holat) va kutilmagan lekin haqiqiy holat.
Maslahatni ko'rsatish
Oddiy, sof funksiyadan boshlang (masalan, valyuta formatlash yoki parol validatsiyasi) — tashqi bog'liqliksiz funksiyalar unit test yozishni o'rganish uchun eng oson boshlang'ich nuqta.