aidoc.uz

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:

Unit testlar
Eng ko'p sonli, eng tez (millisekundlar), tashqi bog'liqliksiz — biznes mantiqni tekshiradi
Widget testlar
O'rtacha tezlik, Flutter framework simulyatsiyasida ishlaydi — UI xatti-harakatini tekshiradi
Integration testlar
Eng kam sonli, eng sekin, real qurilma/emulyatorda ishlaydi — to'liq foydalanuvchi oqimini tekshiradi

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, Navigator o'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.dart

Amaliy 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/ViewModel qatlamida) — 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#

VaziyatJunior darajadagi yondashuvSenior 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.

Amaliy topshiriq

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.