12-bo‘lim
Testlash asoslari
Test piramidasi, modul va integratsiya testlari, soxta obyektlar, qoplama va yaxshi test yozish qoidalari.
Ushbu bo‘lim mundarijasi
Test - kodingiz haqiqatan ishlashining yagona ishonchli dalili. "Menda ishlaydi" - dalil emas.
Nima uchun test kerak? #
- Xatolarni topish - eng aniq vazifa
- Hujjat - test kodning qanday ishlatilishini ko'rsatadi
- Dizaynni yaxshilash - testlab bo'lmaydigan kod odatda yomon loyihalangan
- Ishonch berish - refaktoring va o'zgartirish qo'rqinchli bo'lmaydi
Test piramidasi #
Ko'p jamoalarda piramida teskari bo'ladi: ko'p E2E test, kam modul test.
Natija:
- Test to'plami soatlab ishlaydi
- Testlar beqaror - goh o'tadi, goh o'tmaydi
- Xato qayerda ekanini aniqlash qiyin
- Jamoa testlarga ishonmay qo'yadi va ularni e'tiborsiz qoldiradi
Bu holatga tushmaslik uchun modul testlaridan boshlang.
Modul testi #
Bitta klass yoki funksiyani izolyatsiyada tekshiradi.
// Tekshiriladigan kod
final class Savat
{
private array $elementlar = [];
public function qoshing(int $mahsulotId, Pul $narx, int $soni): void
{
if ($soni < 1) {
throw new InvalidArgumentException('Soni kamida 1 bo\'lishi kerak');
}
if (isset($this->elementlar[$mahsulotId])) {
$this->elementlar[$mahsulotId]['soni'] += $soni;
return;
}
$this->elementlar[$mahsulotId] = ['narx' => $narx, 'soni' => $soni];
}
public function ochiring(int $mahsulotId): void
{
unset($this->elementlar[$mahsulotId]);
}
public function jami(): Pul
{
$yigindi = Pul::nol();
foreach ($this->elementlar as $element) {
$yigindi = $yigindi->qoshing($element['narx']->kopaytiring($element['soni']));
}
return $yigindi;
}
public function boshmi(): bool
{
return $this->elementlar === [];
}
}
use PHPUnit\Framework\TestCase;
final class SavatTest extends TestCase
{
public function test_yangi_savat_bosh_boladi(): void
{
$savat = new Savat();
$this->assertTrue($savat->boshmi());
$this->assertEquals(0, $savat->jami()->tiyin);
}
public function test_mahsulot_qoshilganda_jami_hisoblanadi(): void
{
$savat = new Savat();
$savat->qoshing(1, Pul::somdan(100_000), 2);
$this->assertFalse($savat->boshmi());
$this->assertEquals(200_000, $savat->jami()->som());
}
public function test_bir_xil_mahsulot_soni_qoshiladi(): void
{
$savat = new Savat();
$savat->qoshing(1, Pul::somdan(50_000), 2);
$savat->qoshing(1, Pul::somdan(50_000), 3);
$this->assertEquals(250_000, $savat->jami()->som());
}
public function test_manfiy_soni_xato_beradi(): void
{
$savat = new Savat();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Soni kamida 1');
$savat->qoshing(1, Pul::somdan(50_000), -1);
}
public function test_ochirilgan_mahsulot_jamiga_kirmaydi(): void
{
$savat = new Savat();
$savat->qoshing(1, Pul::somdan(100_000), 1);
$savat->qoshing(2, Pul::somdan(200_000), 1);
$savat->ochiring(1);
$this->assertEquals(200_000, $savat->jami()->som());
}
}
vendor/bin/phpunit tests/SavatTest.php
PHPUnit 11.2.0 by Sebastian Bergmann and contributors.
..... 5 / 5 (100%)
Time: 00:00.024, Memory: 6.00 MB
OK (5 tests, 8 assertions)
AAA tuzilishi #
public function test_chegirma_qollanadi(): void
{
// Arrange - tayyorlash
$savat = new Savat();
$savat->qoshing(1, Pul::somdan(100_000), 5);
$chegirma = new FoizChegirmasi(10);
// Act - amal
$jami = $chegirma->qollang($savat->jami());
// Assert - tekshirish
$this->assertEquals(450_000, $jami->som());
}
- Arrange - boshlang'ich holatni tayyorlash
- Act - tekshiriladigan amalni bajarish
- Assert - natijani tekshirish
Gherkin dagi Given-When-Then bilan bir xil g'oya.
Agar testda ikkita "Act" bo'lsa - ehtimol uni ikkita testga bo'lish kerak.
Yaxshi test qanday bo'ladi? #
| Harf | Ma'nosi | Izoh |
|---|---|---|
| F | Fast | Tez - millisoniyalarda |
| I | Independent | Mustaqil - tartibga bog'liq emas |
| R | Repeatable | Takrorlanuvchi - har doim bir xil natija |
| S | Self-validating | O'zi baholaydi - o'tdi yoki o'tmadi |
| T | Timely | O'z vaqtida - kod bilan birga yoziladi |
// YOMON: mustaqil emas
private static array $malumot = [];
public function test_birinchi(): void
{
self::$malumot[] = 'a';
$this->assertCount(1, self::$malumot);
}
public function test_ikkinchi(): void
{
self::$malumot[] = 'b';
$this->assertCount(2, self::$malumot); // birinchi testga bog'liq!
}
// YOMON: takrorlanmaydigan
public function test_chegirma(): void
{
$chegirma = new BayramChegirmasi();
// Bugun bayram bo'lsa o'tadi, aks holda o'tmaydi
$this->assertEquals(10, $chegirma->foiz());
}
// YAXSHI: vaqt kiritiladi
public function test_navroz_kunida_chegirma_beriladi(): void
{
$chegirma = new BayramChegirmasi(new DateTimeImmutable('2026-03-21'));
$this->assertEquals(10, $chegirma->foiz());
}
$narx = rand(1000, 100000); // test goh o'tadi, goh o'tmaydi
Agar test bugun o'tib, ertaga o'tmasa - uni tuzatish deyarli imkonsiz.
Aniq qiymatlarni ishlating yoki seed o'rnating.
Nomlash #
// YOMON
public function test1(): void { }
public function testSavat(): void { }
public function testJami(): void { }
// YAXSHI - nom nima tekshirilayotganini aytadi
public function test_bosh_savatda_jami_nolga_teng(): void { }
public function test_bir_xil_mahsulot_ikki_marta_qoshilsa_soni_yigiladi(): void { }
public function test_manfiy_soni_berilganda_istisno_tashlanadi(): void { }
public function test_zaxira_yetmasa_buyurtma_yaratilmaydi(): void { }
test_<shart>_<kutilgan natija>
Test o'tmaganda uning nomi muammoni tushuntirishi kerak - kodga qarash shart bo'lmasin.
Parametrlangan testlar #
/**
* @dataProvider chegirmaHolatlari
*/
public function test_chegirma_togri_hisoblanadi(
int $jami,
string $daraja,
int $kutilgan,
): void {
$hisoblagich = new ChegirmaHisoblagichi();
$natija = $hisoblagich->hisoblang(Pul::somdan($jami), $daraja);
$this->assertEquals($kutilgan, $natija->som());
}
public static function chegirmaHolatlari(): array
{
return [
'oddiy mijoz chegirmasiz' => [100_000, 'oddiy', 0],
'kumush 5 foiz' => [100_000, 'kumush', 5_000],
'oltin 10 foiz' => [100_000, 'oltin', 10_000],
'nol summa' => [0, 'oltin', 0],
'katta summa' => [10_000_000, 'oltin', 1_000_000],
];
}
OK (5 tests, 5 assertions)
Massiv kalitlari test nomiga qo'shiladi:
Failed: test_chegirma_togri_hisoblanadi with data set "oltin 10 foiz"
Bu qaysi holat buzilganini darhol ko'rsatadi.
Soxta obyektlar #
final class BuyurtmaServisi
{
public function __construct(
private BuyurtmaRepozitoriyi $repozitoriy,
private OmborServisi $ombor,
private XabarnomaInterfeysi $xabarnoma,
) {}
public function yarating(int $mijozId, array $elementlar): Buyurtma
{
foreach ($elementlar as $element) {
if (!$this->ombor->yetarlimi($element['id'], $element['soni'])) {
throw new ZaxiraYetarliEmas($element['id']);
}
}
$buyurtma = $this->repozitoriy->saqlang(
Buyurtma::yangi($mijozId, $elementlar)
);
$this->xabarnoma->buyurtmaYaratildi($buyurtma);
return $buyurtma;
}
}
final class BuyurtmaServisiTest extends TestCase
{
public function test_zaxira_yetmasa_istisno_tashlanadi(): void
{
// Stub - belgilangan javob qaytaradi
$ombor = $this->createStub(OmborServisi::class);
$ombor->method('yetarlimi')->willReturn(false);
$servis = new BuyurtmaServisi(
$this->createStub(BuyurtmaRepozitoriyi::class),
$ombor,
$this->createStub(XabarnomaInterfeysi::class),
);
$this->expectException(ZaxiraYetarliEmas::class);
$servis->yarating(1, [['id' => 42, 'soni' => 5]]);
}
public function test_buyurtma_yaratilganda_xabar_yuboriladi(): void
{
$ombor = $this->createStub(OmborServisi::class);
$ombor->method('yetarlimi')->willReturn(true);
$repozitoriy = $this->createStub(BuyurtmaRepozitoriyi::class);
$repozitoriy->method('saqlang')->willReturnArgument(0);
// Mock - chaqirilganini tekshiradi
$xabarnoma = $this->createMock(XabarnomaInterfeysi::class);
$xabarnoma->expects($this->once())
->method('buyurtmaYaratildi');
$servis = new BuyurtmaServisi($repozitoriy, $ombor, $xabarnoma);
$servis->yarating(1, [['id' => 42, 'soni' => 2]]);
}
}
Fake - ko'pincha eng yaxshi tanlov #
final class XotiradaBuyurtmaRepozitoriyi implements BuyurtmaRepozitoriyi
{
private array $buyurtmalar = [];
private int $keyingiId = 1;
public function saqlang(Buyurtma $buyurtma): Buyurtma
{
$id = $buyurtma->id ?? $this->keyingiId++;
$saqlangan = $buyurtma->idBilan($id);
$this->buyurtmalar[$id] = $saqlangan;
return $saqlangan;
}
public function toping(int $id): ?Buyurtma
{
return $this->buyurtmalar[$id] ?? null;
}
public function mijozBoyicha(int $mijozId): array
{
return array_values(array_filter(
$this->buyurtmalar,
fn (Buyurtma $b) => $b->mijozId === $mijozId,
));
}
}
| Mock | Fake | |
|---|---|---|
| Sozlash | Har testda | Bir marta yoziladi |
| Test o'qilishi | Sozlash bilan to'lgan | Toza |
| Refaktoringda | Buziladi | Ishlayveradi |
| Haqiqiy xatti-harakat | Yo'q | Ha |
Ko'p testda ishlatiladigan bog'liqlik uchun Fake yozing.
// Bu test nimani tekshiryapti?
$a = $this->createMock(A::class);
$b = $this->createMock(B::class);
$c = $this->createMock(C::class);
$a->method('x')->willReturn($b);
$b->method('y')->willReturn($c);
$c->method('z')->willReturn(42);
$this->assertEquals(42, $servis->hisobla());
Bu test kod tuzilishini tekshiradi, xatti-harakatni emas. Refaktoring qilsangiz - test buziladi, garchi kod to'g'ri ishlasa ham.
Bunday test foydadan ko'ra zarar keltiradi.
Integratsiya testi #
final class BuyurtmaRepozitoriyiTest extends TestCase
{
private PDO $pdo;
private MysqlBuyurtmaRepozitoriyi $repozitoriy;
protected function setUp(): void
{
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->exec(file_get_contents(__DIR__ . '/../database/sxema.sql'));
$this->repozitoriy = new MysqlBuyurtmaRepozitoriyi($this->pdo);
}
public function test_saqlangan_buyurtma_topiladi(): void
{
$buyurtma = Buyurtma::yangi(mijozId: 1);
$buyurtma->elementQoshing(new Mahsulot(42, 'Klaviatura', Pul::somdan(250_000)), 2);
$saqlangan = $this->repozitoriy->saqlang($buyurtma);
$topilgan = $this->repozitoriy->toping($saqlangan->id);
$this->assertNotNull($topilgan);
$this->assertEquals(500_000, $topilgan->jami()->som());
$this->assertCount(1, $topilgan->elementlar());
}
public function test_mavjud_bolmagan_buyurtma_null_qaytaradi(): void
{
$this->assertNull($this->repozitoriy->toping(99999));
}
}
Uchta yondashuv:
- Xotiradagi SQLite - eng tez, lekin MySQL dan farq qiladi
- Tranzaksiya - har testdan keyin
rollBack() - Docker konteyner - haqiqiy MySQL, sekinroq
Ishlab chiqarishda MySQL ishlatsangiz, integratsiya testlarida ham MySQL ishlatish yaxshiroq - SQLite ba'zi farqlarni yashiradi.
API testi #
final class BuyurtmaApiTest extends TestCase
{
public function test_buyurtma_yaratish_201_qaytaradi(): void
{
$javob = $this->post('/api/v1/buyurtmalar', [
'elementlar' => [['mahsulot_id' => 42, 'soni' => 2]],
'manzil_id' => 5,
], ['Authorization' => 'Bearer ' . $this->token()]);
$this->assertEquals(201, $javob->kod());
$this->assertArrayHasKey('id', $javob->json()['malumot']);
$this->assertEquals('yangi', $javob->json()['malumot']['holat']);
}
public function test_avtorizatsiyasiz_401_qaytaradi(): void
{
$javob = $this->post('/api/v1/buyurtmalar', []);
$this->assertEquals(401, $javob->kod());
}
public function test_notogri_malumot_422_qaytaradi(): void
{
$javob = $this->post('/api/v1/buyurtmalar', [
'elementlar' => [['mahsulot_id' => 42, 'soni' => -5]],
], ['Authorization' => 'Bearer ' . $this->token()]);
$this->assertEquals(422, $javob->kod());
$this->assertEquals('validatsiya_xatosi', $javob->json()['xato']['kod']);
}
}
E2E testi #
// tests/e2e/buyurtma.spec.js
import { test, expect } from '@playwright/test';
test('foydalanuvchi buyurtma bera oladi', async ({ page }) => {
await page.goto('/kirish');
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="parol"]', 'sinovParoli123');
await page.click('button[type="submit"]');
await page.goto('/katalog');
await page.click('[data-test="mahsulot-42"] [data-test="savatga-qoshish"]');
await expect(page.locator('[data-test="savat-soni"]')).toHaveText('1');
await page.goto('/savat');
await page.click('[data-test="buyurtma-berish"]');
await page.selectOption('[name="manzil_id"]', '5');
await page.click('[data-test="tasdiqlash"]');
await expect(page.locator('[data-test="muvaffaqiyat"]')).toBeVisible();
await expect(page).toHaveURL(/\/buyurtmalar\/\d+/);
});
data-test atributlarini ishlating<button class="btn btn-primary" data-test="savatga-qoshish">Savatga</button>
CSS klasslar dizayn o'zgarganda o'zgaradi. data-test esa faqat
testlar uchun - u barqaror qoladi.
Sabablari: tarmoq kechikishi, animatsiya, asinxron yuklash.
Yechimlar:
// YOMON: qat'iy kutish
await page.waitForTimeout(2000);
// YAXSHI: shartni kutish
await expect(page.locator('[data-test="natija"]')).toBeVisible();
Doimiy beqaror testni o'chiring - u jamoaning testlarga ishonchini yo'qotadi.
Qoplama #
vendor/bin/phpunit --coverage-html hisobot/
vendor/bin/phpunit --coverage-text
Code Coverage Report:
Summary:
Classes: 68.42% (13/19)
Methods: 81.25% (78/96)
Lines: 84.31% (612/726)
App\Domen\Savat
Methods: 100.00% ( 8/ 8) Lines: 100.00% ( 42/ 42)
App\Servislar\BuyurtmaServisi
Methods: 85.71% ( 6/ 7) Lines: 91.30% ( 63/ 69)
App\Infratuzilma\EmailYuboruvchi
Methods: 0.00% ( 0/ 4) Lines: 0.00% ( 0/ 31)
public function bolish(int $a, int $b): float
{
return $a / $b;
}
public function test_bolish(): void
{
$this->assertEquals(5, $this->hisoblagich->bolish(10, 2));
}
100% qoplama - lekin nolga bo'lish tekshirilmagan.
Qoplama nimaning testlanmaganini ko'rsatadi, lekin testlanganning yaxshi testlanganini kafolatlamaydi.
| Qism | Maqsad |
|---|---|
| Biznes mantiq (domen, servislar) | 90%+ |
| Kontrollerlar | 70%+ |
| Infratuzilma (adapterlar) | 50%+ |
| Umumiy | 70-80% |
100% ga intilish - vaqtni behuda sarflash. Muhim qismlarga diqqat qiling.
Nimani testlash kerak? #
- Biznes qoidalari - eng muhim
- Chekka holatlar: bo'sh, nol, manfiy, juda katta
- Xato yo'llari - istisnolar to'g'ri tashlanadimi
- Tuzatilgan xatolar - qaytmasligi uchun
- Murakkab algoritmlar
- Integratsiyalar - tashqi tizimlar bilan
- Getter va setter lar
- Freymvork kodi (u allaqachon testlangan)
- Tashqi kutubxonalar
- Oddiy konfiguratsiya
- Faqat
returnqiladigan metodlar
Bunday testlar vaqt oladi, lekin hech nima kafolatlamaydi.
Testlarni tashkil qilish #
tests/
├── Modul/
│ ├── Domen/
│ │ ├── SavatTest.php
│ │ ├── PulTest.php
│ │ └── BuyurtmaTest.php
│ └── Servislar/
│ └── BuyurtmaServisiTest.php
├── Integratsiya/
│ ├── Repozitoriylar/
│ └── Api/
├── E2E/
├── Yordamchilar/
│ ├── XotiradaBuyurtmaRepozitoriyi.php
│ └── BuyurtmaQuruvchi.php
└── bootstrap.php
// Test ma'lumotlarini yaratish uchun quruvchi
final class BuyurtmaQuruvchi
{
private int $mijozId = 1;
private BuyurtmaHolati $holat = BuyurtmaHolati::Yangi;
private array $elementlar = [];
public static function yangi(): self
{
return new self();
}
public function mijoz(int $id): self
{
$this->mijozId = $id;
return $this;
}
public function holat(BuyurtmaHolati $holat): self
{
$this->holat = $holat;
return $this;
}
public function element(int $mahsulotId, int $narx, int $soni = 1): self
{
$this->elementlar[] = compact('mahsulotId', 'narx', 'soni');
return $this;
}
public function quring(): Buyurtma
{
// ...
}
}
public function test_tolangan_buyurtmani_bekor_qilish_mumkin(): void
{
$buyurtma = BuyurtmaQuruvchi::yangi()
->holat(BuyurtmaHolati::Tolangan)
->element(42, 250_000, 2)
->quring();
$buyurtma->bekorQiling('Mijoz so\'radi');
$this->assertEquals(BuyurtmaHolati::Bekor, $buyurtma->holat());
}
Testda faqat muhim ma'lumotni ko'rsating, qolganini standart qiymatlar to'ldirsin.
Test o'qiganda "bu yerda holat muhim" ekani darhol ko'rinadi.
Savatklassini yozing va unga 8 ta modul testi yozing.- AAA tuzilishiga rioya qiling.
- Chekka holatlarni testlang: bo'sh savat, nol soni, manfiy soni.
- Parametrlangan test yozing (
dataProviderbilan). - Tashqi bog'liqlik uchun Stub yarating.
- Xotiradagi Fake repozitoriy yozing.
- Baza bilan integratsiya testi yozing.
- API endpoint uchun test yozing (200, 401, 422 holatlari).
- Qoplama hisobotini yarating va testlanmagan qismlarni toping.
- Test ma'lumotlari quruvchisini yozing.
Xulosa #
- Test - kod ishlashining yagona ishonchli dalili.
- Test piramidasi: ko'p modul, kam integratsiya, juda kam E2E.
- Teskari piramida ("muzqaymoq") - sekin va beqaror testlarga olib keladi.
- AAA: Arrange, Act, Assert.
- FIRST: tez, mustaqil, takrorlanuvchi, o'zi baholaydigan, o'z vaqtida.
- Test nomi nima tekshirilayotganini aytsin.
dataProviderbilan bir xil mantiqni ko'p holatda tekshiring.- Fake ko'pincha Mock dan yaxshiroq - test refaktoringda buzilmaydi.
- Mock ni haddan tashqari ishlatish testni kod tuzilishiga bog'laydi.
- E2E testlarda
data-testatributlarini ishlating. - Qoplama nimaning testlanmaganini ko'rsatadi, sifatni emas.
- Getter, freymvork va kutubxona kodini testlamang.
Keyingi bo'limda TDD yondashuvini o'rganamiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.