22 β PHPUnit chuqur va test doubles¶
β¬ οΈ Oldingi: 21 β Taktik dizayn: Repository, Service, DTO Β· π README Β· Keyingi: 23 β Pest, integratsiya, coverage va mutation β‘οΈ
Bu bobda: boshlovchi kitobdagi testing bobi sizga PHPUnit'ning birinchi qadamini β bitta funksiya, bitta
assertSameβ ko'rsatdi. Bu bob shu poydevorni ekspert darajaga ko'taradi. Avval nega test yozamiz degan savolga jiddiy javob beramiz: regressiya himoyasi, refactoring ishonchi va eng nozigi β dizaynga bosim (test yozish qiyin bo'lsa, demak dizayn yomon). So'ng test yozishning universal grammatikasi β AAA (Arrange-Act-Assert) va test piramidasi (ko'p unit < integration < kam e2e) hamda uning teskarisi bo'lgan "muzqaymoq konus" anti-patterni. PHPUnit mexanikasini chuqur ochamiz:TestCase, assertion'lar va eng ko'p xato keltiradiganassertSamevsassertEqualsfarqi, zamonaviy PHP 8 atributlari (#[Test],#[DataProvider],#[Depends],#[Group]),setUp/tearDownhayot tsikli. Bobning yuragi β test doubles taksonomiyasi: dummy, stub, spy, mock, fake β har biriga aniq ta'rif, qachon ishlatishni va PHPUnit'da qanday yasashni (createStub/createMock,expects()->method()->with()->willReturn()) ko'rsatamiz. Keyin falsafiy savol: qachon mock qilmaslik kerak β London (mockist) va Chicago (classicist) maktablari. Oxirida test smells (mo'rt, sekin, mystery guest, bog'liq testlar). Hamma test sinflari haqiqiyphp 8.4+phpunit/phpunit12.5 bilan ishga tushirilib yashil natija olindi (15 test, 30 assertion). Coverage qismi xdebug/pcov yo'qligi sabab illustrativ β buni halol belgilaymiz.
Nega test yozamiz? (jiddiy javob)¶
Boshlovchi kitobda (testing) test yozishning to'rt sababini sanagandik: refactoring xavfsizligi, tirik hujjat, ishonch, xatoni erta topish. Bular to'g'ri, lekin ekspert sifatida uchta sababni chuqurroq tushunish kerak β chunki ular sizning dizayningizga ta'sir qiladi.
1. Regressiya himoyasi β vaqt mashinasidagi qulf¶
"Regressiya" β ilgari ishlagan narsa yangi o'zgarishdan keyin buzilishi. Katta tizimda har bir o'zgarish potensial regressiya. Test to'plami β bu o'tmishdagi to'g'ri xulqning qulfi: bugun yozgan testingiz olti oydan keyin, mutlaqo boshqa odam tomonidan kiritilgan o'zgarish sizning kodingizni buzganini bir soniyada aytib beradi. Testsiz tizimda bu xatoni foydalanuvchi topadi β productionda, eng yomon paytda.
2. Refactoring ishonchi β yashil ekran ostida tozalash¶
Refactoring β xulqni o'zgartirmasdan ichki tuzilishni yaxshilash. Lekin "xulqni o'zgartirmadim" degan da'voni qanday isbotlaysiz? Testlar bilan. Yashil test to'plami ostida siz sinf ichini ag'dar-to'ntar qilishingiz mumkin β agar testlar yashil qolsa, tashqi xulq saqlangan. Testsiz refactoring β bu ko'r-ko'rona jarrohlik.
3. Dizaynga bosim β eng kam tushuniladigan, eng qimmatli foyda¶
Bu eng muhim va eng kam gapiriladigan nuqta:
Agar sinfni test qilish qiyin bo'lsa β bu testning emas, dizaynning muammosi.
Testni yozolmayotgan bo'lsangiz, odatda sabab quyidagilardan biri:
- Sinf o'z bog'liqliklarini o'zi yaratadi (
new PdoConnection(...)konstruktor ichida) β uni almashtira olmaysiz. Yechim: Dependency Injection (13-bob). - Sinf juda ko'p ish qiladi (God object) β bitta testda o'nta narsani sozlash kerak. Yechim: mas'uliyatni bo'lish (SRP, toza kod).
- Mantiq global holatga (
static,$_SESSION,date()) bog'langan β izolyatsiya qila olmaysiz. Yechim: holatni argument yoki interfeys orqali in'ektsiya qilish.
Shuning uchun tajribali muhandislar aytadi: test β dizayn vositasi, faqat tekshiruv emas. Test yozish qiyinligini "dizayn yomon" signali sifatida o'qing. Aynan shu sabab TDD (Test-Driven Development) β avval test yozib, keyin kod β dizaynni yaxshilaydi: testlanmaydigan kod yozolmaysiz, chunki test allaqachon bor.
AAA: har bir testning skeletini¶
Yaxshi test bir g'oyaviy tuzilishga ega β Arrange-Act-Assert (Tayyorla-Bajar-Tasdiqla). Bu uchlik testni o'qiladigan va niyatli qiladi:
<?php
declare(strict_types=1);
namespace Tests;
use App\Money;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class MoneyAaaTest extends TestCase
{
#[Test]
public function ikki_summa_qoshiladi(): void
{
// Arrange β sahnani tayyorla (kirish ma'lumoti, obyektlar)
$a = new Money(500, 'USD');
$b = new Money(250, 'USD');
// Act β tekshirilayotgan AMALNI bajar (bitta chaqiruv)
$sum = $a->add($b);
// Assert β natijani kutilgan bilan solishtir
$this->assertSame(750, $sum->amount);
$this->assertSame('USD', $sum->currency);
}
}
Qoidalar:
- Bitta Act. Bir testda bitta amalni tekshiring. Ikkita
add()chaqirsangiz β qaysi biri buzilganini test aytmaydi. - Aniq chegaralar. Bo'sh qator yoki izoh bilan uch bosqichni ajrating β o'qiyotgan odam "qayerda tayyorlash tugab, tekshirish boshlandi" deb chalkashmaydi.
- Niyatli nom. Test nomi xulqni tasvirlasin:
ikki_summa_qoshiladi,manfiy_summa_xato_tashlaydiβtestMoney1emas.
Bu bobda ishlatadigan SUT ("System Under Test" β tekshirilayotgan sinf) β soddalashtirilgan e-commerce domeni: Money value object (06-bob bilan bog'liq), Order, va to'lov qatlami.
<?php
declare(strict_types=1);
namespace App;
final readonly class Money
{
public function __construct(
public int $amount, // tiyin (eng kichik birlik) β float EMAS!
public string $currency,
) {
if ($amount < 0) {
throw new \InvalidArgumentException('Manfiy summa mumkin emas');
}
if (strlen($currency) !== 3) {
throw new \InvalidArgumentException('Valyuta kodi 3 harf bo`lishi kerak');
}
}
public function add(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Valyutalar mos emas');
}
return new self($this->amount + $other->amount, $this->currency);
}
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}
Test piramidasi va "muzqaymoq konus"¶
Testlar bir xil emas. Ular doirasi (scope) va narxi bo'yicha qatlamlarga bo'linadi. Sog'lom taqsimotni test piramidasi ko'rsatadi.
- Unit testlar (asos, ko'p). Bitta sinf yoki funksiyani izolyatsiyada tekshiradi β barcha tashqi bog'liqliklar test double bilan almashtiriladi. Millisekundlarda ishlaydi, barqaror (flaky emas), aniq aybdorni ko'rsatadi. Test to'plamingizning 80% shu yerda bo'lishi kerak.
- Integration testlar (o'rta). Bir nechta komponent birga ishlashini tekshiradi β masalan, repository haqiqiy SQLite bazasi bilan, yoki middleware pipeline butun PSR-7 so'rovi bilan. Sekinroq (DB/fayl I/O), lekin "qismlarni ulaganda buzilarmikan" savoliga javob beradi. Buni 23-bobda
:memory:SQLite bilan chuqur ko'ramiz. - E2E testlar (uch, kam). Butun tizimni foydalanuvchi ko'zi bilan β brauzer orqali, real HTTP. Eng qimmat: sekin, mo'rt (tarmoq, UI o'zgarishi), nosozlik sababini topish qiyin. Faqat eng muhim "happy path"larni qoplaydi.
Anti-pattern: "muzqaymoq konus" (ice cream cone)¶
Piramidani ag'darib qo'ysangiz β tepada ko'p e2e, pastda kam unit β bu "muzqaymoq konus" anti-patterni. Oqibatlari:
- Sekin CI. E2E sekin; minglab e2e bilan test to'plami 40 daqiqa ishlaydi β dasturchilar uni o'tkazib yuboradi.
- Mo'rtlik (flakiness). E2E tarmoq/timing'ga bog'liq β goh yashil, goh qizil. Ishonchni yo'qotadi: "yana o'sha test yiqildi, ahamiyat berma" β bu o'lim.
- Yomon diagnostika. E2E "checkout ishlamadi" deydi, lekin qayerda buzildi β to'lov, savatcha, baza? β aytmaydi. Unit test esa to'g'ri sinfni ko'rsatadi.
Qoida: mantiqni unit darajada test qiling, ulanishni integration darajada, faqat kritik oqimni e2e darajada.
Ko'prik. Bu bobning hamma misollari unit qatlamda. Integration (real SQLite repository, HTTP) va coverage/mutation 23-bobda. E2E (brauzer) bu PHP trakidan tashqarida β uni alohida vositalar (Panther, Cypress) qoplaydi.
PHPUnit mexanikasi: TestCase va atributlar¶
Avval o'rnatamiz (faqat --dev, productionga ketmaydi):
Bu PHPUnit 12.x ni vendor/ ga yuklaydi. composer.json:
Konfiguratsiya: phpunit.xml¶
Loyiha ildizida phpunit.xml β qaysi papkada testlar, qanday qoidalar:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
failOnRisky="true"
failOnWarning="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
failOnRisky va failOnWarning β ekspert sozlamasi: "risky" test (masalan hech narsa assert qilmaydigan test) yoki ogohlantirish ham CI'ni qizil qilsin. Bu sekin-asta yomonlashishni oldini oladi.
Zamonaviy atributlar (PHP 8) β annotatsiyalar o'rnida¶
Eski PHPUnit @test, @dataProvider kabi doc-comment annotatsiyalarini ishlatardi. PHP 8 dan boshlab ular atributlar bilan almashtirildi β bu IDE tomonidan tekshiriladi, xato terilsa darrov ko'rinadi:
| Atribut | Vazifasi |
|---|---|
#[Test] |
Metod test ekanini bildiradi (test prefiksisiz nom mumkin) |
#[DataProvider('metod')] |
Bitta testni turli ma'lumotlar bilan qayta ishga tushiradi |
#[Depends('boshqaTest')] |
Test boshqa testdan keyin va uning natijasiga ega bo'lib ishlaydi |
#[Group('nom')] |
Testni guruhga belgilaydi (--group nom bilan tanlab ishga tushirish) |
<?php
declare(strict_types=1);
namespace Tests;
use App\Money;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class MoneyTest extends TestCase
{
#[Test]
public function ikki_summa_qoshiladi(): void
{
$a = new Money(500, 'USD');
$b = new Money(250, 'USD');
$sum = $a->add($b);
$this->assertSame(750, $sum->amount);
$this->assertSame('USD', $sum->currency);
}
// Data provider β STATIC metod, iterable qaytaradi.
// Kalit ('nol + nol') test nomida ko'rinadi -> diagnostika oson.
/** @return iterable<string, array{int, int, int}> */
public static function summaProvider(): iterable
{
yield 'nol + nol' => [0, 0, 0];
yield 'oddiy' => [100, 200, 300];
yield 'katta' => [999_999, 1, 1_000_000];
}
#[Test]
#[DataProvider('summaProvider')]
#[Group('arifmetika')]
public function turli_summalar_qoshiladi(int $x, int $y, int $expected): void
{
$sum = (new Money($x, 'USD'))->add(new Money($y, 'USD'));
$this->assertSame($expected, $sum->amount);
}
}
Data provider β takror kodni o'ldiradi. Uchta o'xshash test o'rniga bitta test + uchta ma'lumot to'plami. Har bir to'plam alohida test sifatida hisoblanadi: biri yiqilsa, qaysi ma'lumot ayblini test nomi (with "katta") aytadi.
Diqqat: data provider metodi
public staticbo'lishi shart (PHPUnit 10+). Aks holda "deprecation" ogohlantirishi, vafailOnWarningbilan test qulaydi.
assertSame vs assertEquals β eng ko'p xato keltiradigan farq¶
Bu ikkala assertion'ning farqini tushunmaslik β yangi (va hatto tajribali) dasturchilarning eng keng tarqalgan tuzog'i. Qisqacha:
assertEquals($a, $b)β==semantikasi (qiymat teng, tip moslashtiriladi).assertSame($a, $b)β===semantikasi (qiymat va tip aynan teng; obyektlar uchun β ayni bitta instance).
Bu farqning oqibati == vs === tuzog'i bilan bir xil (05-bobda chuqur ko'rilgan):
<?php
declare(strict_types=1);
namespace Tests;
use App\Money;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class AssertSameVsEqualsTest extends TestCase
{
#[Test]
public function assertSame_va_assertEquals_farqi(): void
{
$a = new Money(500, 'USD');
$b = new Money(500, 'USD');
// assertEquals: == bilan -> ikki ALOHIDA obyekt, lekin holati teng -> OK
$this->assertEquals($a, $b);
// assertSame: === bilan -> ular ayni bitta instance EMAS -> NotSame
$this->assertNotSame($a, $b);
// Skalyar uchun assertSame tip + qiymatni tekshiradi
$this->assertSame(500, $a->amount); // int 500 === int 500
}
}
Nega bu muhim? Skalyar qiymatlarda assertEquals xavfli:
// β Bu yashil bo'ladi, lekin BUG yashiradi:
$this->assertEquals('0', 0); // '0' == 0 -> true (tip coercion)
$this->assertEquals('', null); // '' == null -> true (eski PHP'da)
// β
assertSame buni ushlaydi:
// $this->assertSame('0', 0); // FAIL: string('0') !== int(0)
Ekspert qoidasi: standart holatda
assertSameishlating. U tip xatolarini ushlaydi (funksiya'0'o'rniga0qaytarib qo'yganini ko'rsatadi).assertEqualsni faqat ataylab β masalan ikki alohida value object holatini solishtirganda β ishlating. Float'lar uchun esaassertEqualsWithDelta($expected, $actual, 0.0001)(suzuvchi nuqta aniqligi sabab===ishlamaydi).
Boshqa muhim assertion'lar¶
$this->assertTrue($x); $this->assertFalse($x);
$this->assertNull($x); $this->assertNotNull($x);
$this->assertCount(3, $array); // sanog'i aynan 3
$this->assertContains('a', $array); // element bor
$this->assertInstanceOf(Money::class, $obj);
$this->assertStringContainsString('xato', $msg);
$this->assertGreaterThan(0, $n); $this->assertLessThanOrEqual(10, $n);
Istisnoni (exception) tekshirish¶
Xato tashlashini test qilish β expectException Act'dan oldin chaqiriladi:
<?php
declare(strict_types=1);
namespace Tests;
use App\Money;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class MoneyExceptionTest extends TestCase
{
#[Test]
public function manfiy_summa_xato_tashlaydi(): void
{
// Bu MIQDORLAR Act'dan oldin e'lon qilinadi
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Manfiy summa'); // xabar shu matnni o'z ichiga olsin
new Money(-1, 'USD'); // Act: xato tashlasa -> test yashil
}
}
Agar new Money(-1, ...) xato tashlamasa, test yiqiladi ("kutilgan istisno tashlanmadi"). Bu "negativ test" β noto'g'ri kirishda kod to'g'ri yiqilishini kafolatlaydi.
Hayot tsikli: setUp, tearDown, #[Depends]¶
Ko'p testda bir xil tayyorgarlik takrorlanadi (Arrange qismi). setUp() β har bir test metodidan oldin ishlaydigan ilmoq, tearDown() β har biridan keyin (tozalash uchun):
<?php
declare(strict_types=1);
namespace Tests;
use App\Order;
use App\Money;
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class DependsLifecycleTest extends TestCase
{
private Order $order;
protected function setUp(): void
{
// Har testdan OLDIN yangi, TOZA buyurtma -> testlar bir-biridan mustaqil
$this->order = new Order('ord-fixture', 'USD');
}
protected function tearDown(): void
{
// Har testdan KEYIN tozalash (fayl o'chirish, ulanish yopish va h.k.)
unset($this->order);
}
#[Test]
public function bosh_buyurtma_yaratiladi(): Order
{
$this->assertSame(0, $this->order->lineCount());
$this->order->addLine('Kitob', new Money(2000, 'USD'));
return $this->order; // keyingi testga uzatish uchun qaytaramiz
}
#[Test]
#[Depends('bosh_buyurtma_yaratiladi')]
public function oldingi_testdan_kelgan_buyurtma_ishlatiladi(Order $order): void
{
// Depends qaytargan qiymat argument sifatida keladi
$this->assertSame(1, $order->lineCount());
$this->assertSame(2000, $order->total()->amount);
}
}
Muhim nuance: setUp() har test uchun yangi $this->order yaratadi β bu izolyatsiyani kafolatlaydi (bir testning o'zgarishi boshqasiga oqib o'tmaydi). #[Depends] esa boshqa narsa: u testlararo qiymat uzatadi.
Ogohlantirish β
#[Depends]ni ehtiyot ishlating. U testlarni bir-biriga bog'laydi β bu test smell (quyida ko'ramiz).bosh_buyurtma_yaratiladiyiqilsa, unga bog'liq test o'tkazib yuboriladi (skipped), va xatolar kaskadi yuzaga keladi. Ko'p hollarda yaxshiroq yechim β har testda kerakli holatnisetUpyoki yordamchi metod bilan mustaqil qurish.#[Depends]faqat qimmat tayyorgarlik (masalan sekin tashqi ulanish) takrorlanmasligi kerak bo'lganda oqlanadi.
Statik ilmoqlar¶
setUpBeforeClass() / tearDownAfterClass() β butun sinf uchun bir marta (har test uchun emas). Qimmat, lekin o'zgarmas resurs (masalan sxema yaratilgan test bazasi) uchun.
Test doubles taksonomiyasi¶
"Test double" β kino dublyori kabi: haqiqiy bog'liqlik o'rnida turuvchi soxta obyekt. Maqsad β SUT ni izolyatsiya qilish: testda haqiqiy to'lov shlyuzini chaqirib bo'lmaydi (pul ketadi!), haqiqiy bazaga ulanmaymiz (sekin, holat oqadi). Gerard Meszaros bularni besh turga ajratgan, va bu farqlarni aniq bilish ekspertni boshlovchidan ajratadi.
| Tur | Ta'rif | Qachon |
|---|---|---|
| Dummy | To'ldiruvchi β imzoni to'ldirish uchun beriladi, lekin ishlatilmaydi | Konstruktor argumentini qondirish |
| Stub | Tayyor javob qaytaradi, o'zaro ta'sirni tekshirmaydi | SUT ga kerakli kirish ma'lumotini berish |
| Spy | Chaqiruvni yozib boradi, tekshirish KEYIN | "Chaqirildimi?" ni test ichida tekshirish |
| Mock | Kutilgan o'zaro ta'sirni oldindan belgilab tekshiradi | O'zaro ta'sir (side-effect) muhim bo'lganda |
| Fake | Ishlaydigan soddalashtirilgan implementatsiya | Haqiqiy xulq kerak, lekin yengil (in-memory) |
Bu farqlarni amalda ko'ramiz. SUT β CheckoutService: u to'lov shlyuzini chaqiradi va loglaydi.
<?php
declare(strict_types=1);
namespace App;
final class CheckoutService
{
public function __construct(
private readonly PaymentGateway $gateway, // interfeys -> double bilan almashtiriladi
private readonly Logger $logger,
) {}
public function checkout(Order $order, string $cardToken): Receipt
{
if ($order->lineCount() === 0) {
throw new \DomainException('Bo`sh buyurtmani to`lab bo`lmaydi');
}
$total = $order->total();
$this->logger->info("Checkout boshlandi: {$order->id}");
try {
$txId = $this->gateway->charge($total, $cardToken);
} catch (PaymentFailedException $e) {
$this->logger->error("To`lov muvaffaqiyatsiz: {$order->id}");
throw $e;
}
$this->logger->info("To`lov muvaffaqiyatli: {$txId}");
return new Receipt($order->id, $txId, $total);
}
}
Diqqat: CheckoutService o'z bog'liqliklarini interfeys orqali, konstruktorda oladi (PaymentGateway, Logger). Aynan shu DI (13-bob) ularni testda double bilan almashtirishga imkon beradi. Agar u new StripeGateway() ni ichida yaratsa edi β testlay olmasdik. (Mana yana "dizaynga bosim" β testlanuvchanlik yaxshi dizaynni majbur qiladi.)
Stub va dummy¶
<?php
declare(strict_types=1);
namespace Tests;
use App\CheckoutService;
use App\Logger;
use App\Money;
use App\Order;
use App\PaymentGateway;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CheckoutStubTest extends TestCase
{
// Yordamchi: 2500 USD'lik bitta qatorli buyurtma quradi
private function buyurtma(): Order
{
$order = new Order('ord-1', 'USD');
$order->addLine('Kitob', new Money(2500, 'USD'));
return $order;
}
#[Test]
public function stub_tayyor_javob_qaytaradi(): void
{
// STUB: gateway har doim 'tx-42' qaytaradi (o'zaro ta'sirni TEKSHIRMAYMIZ)
$gateway = $this->createStub(PaymentGateway::class);
$gateway->method('charge')->willReturn('tx-42');
// DUMMY: logger kerak (konstruktor talab qiladi), lekin bu testda ahamiyatsiz
$logger = $this->createStub(Logger::class);
$service = new CheckoutService($gateway, $logger);
$receipt = $service->checkout($this->buyurtma(), 'card_tok');
// Faqat NATIJANI tekshiramiz (holat-asosli tekshiruv)
$this->assertSame('tx-42', $receipt->transactionId);
$this->assertSame(2500, $receipt->paid->amount);
}
}
createStub() β interfeysdan soxta obyekt yasaydi, hamma metodlari "null/bo'sh" qaytaradi. method('charge')->willReturn('tx-42') bilan biz unga tayyor javob beramiz. Bu yerda stub (charge javobini berib turadi) va dummy (logger β kerak, lekin natijaga ta'sir qilmaydi) bir testda. Farq niyatda: stub'dan javob olamiz, dummy esa shunchaki o'rinni to'ldiradi.
Mock β o'zaro ta'sirni tekshirish¶
Ba'zan natija emas, o'zaro ta'sir muhim: "to'lov shlyuzi aynan 2500 USD bilan, aynan bir marta chaqirildimi?" Bu β mock:
<?php
declare(strict_types=1);
namespace Tests;
use App\CheckoutService;
use App\Logger;
use App\Money;
use App\Order;
use App\PaymentGateway;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CheckoutMockTest extends TestCase
{
// Yordamchi: 2500 USD'lik bitta qatorli buyurtma quradi
private function buyurtma(): Order
{
$order = new Order('ord-1', 'USD');
$order->addLine('Kitob', new Money(2500, 'USD'));
return $order;
}
#[Test]
public function mock_kutilgan_ozaro_tasirni_tekshiradi(): void
{
// MOCK: charge() AYNAN 2500 USD bilan, AYNAN bir marta chaqirilishini KUTAMIZ
$gateway = $this->createMock(PaymentGateway::class);
$gateway->expects($this->once()) // aynan 1 marta
->method('charge')
->with( // aynan shu argumentlar bilan
$this->callback(fn (Money $m): bool => $m->amount === 2500 && $m->currency === 'USD'),
'card_tok',
)
->willReturn('tx-99');
$logger = $this->createStub(Logger::class);
$service = new CheckoutService($gateway, $logger);
$receipt = $service->checkout($this->buyurtma(), 'card_tok');
// Verifikatsiya AVTOMATIK: agar charge() chaqirilmasa yoki noto'g'ri arg bilan
// chaqirilsa, PHPUnit test oxirida testni QULATADI
$this->assertSame('tx-99', $receipt->transactionId);
}
}
createStub va createMock farqi tekshirishda: createMock expects() bilan kutilgan chaqiruvlarni belgilaydi va test oxirida ularni avtomatik tasdiqlaydi. expects($this->once()) β "aynan bir marta", boshqalar: $this->never(), $this->exactly(3), $this->atLeastOnce(). with(...) β argumentlar, callback esa murakkab argument (Money obyekti) ichini tekshirish uchun.
Mock'ning kuchi β side-effectni tekshirish, masalan log yozildimi:
<?php
declare(strict_types=1);
namespace Tests;
use App\CheckoutService;
use App\Logger;
use App\Money;
use App\Order;
use App\PaymentFailedException;
use App\PaymentGateway;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CheckoutErrorLogTest extends TestCase
{
// Yordamchi: 2500 USD'lik bitta qatorli buyurtma quradi
private function buyurtma(): Order
{
$order = new Order('ord-1', 'USD');
$order->addLine('Kitob', new Money(2500, 'USD'));
return $order;
}
#[Test]
public function tolov_muvaffaqiyatsiz_bolsa_xato_loglanadi(): void
{
// Stub: charge() istisno tashlaydi
$gateway = $this->createStub(PaymentGateway::class);
$gateway->method('charge')
->willThrowException(new PaymentFailedException('kartada mablag` yetarli emas'));
// MOCK logger: error() AYNAN bir marta, 'muvaffaqiyatsiz' so'zi bilan chaqirilsin
$logger = $this->createMock(Logger::class);
$logger->expects($this->once())
->method('error')
->with($this->stringContains('muvaffaqiyatsiz'));
$service = new CheckoutService($gateway, $logger);
$this->expectException(PaymentFailedException::class);
$service->checkout($this->buyurtma(), 'card_tok');
}
}
Spy β qo'lda yozilgan, keyin tekshiriladigan¶
PHPUnit mock'lari kuchli, lekin ba'zan oddiy spy β chaqiruvni yozib boruvchi qo'lda yozilgan sinf β o'qiladigan bo'ladi. Mock'dan farqi: spy kutilganni oldindan belgilamaydi, balki chaqiruvlarni yozadi va siz keyin (Assert qismida) tekshirasiz:
<?php
declare(strict_types=1);
namespace Tests;
use App\Logger;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
/** Qo'lda yozilgan SPY: chaqiruvlarni YOZIB boradi, lekin o'zi tekshirmaydi. */
final class SpyLogger implements Logger
{
/** @var list<array{level: string, message: string}> */
public array $records = [];
public function info(string $message): void
{
$this->records[] = ['level' => 'info', 'message' => $message];
}
public function error(string $message): void
{
$this->records[] = ['level' => 'error', 'message' => $message];
}
}
final class SpyLoggerTest extends TestCase
{
#[Test]
public function spy_chaqiruvlarni_yozib_boradi(): void
{
// Arrange
$spy = new SpyLogger();
// Act
$spy->info('birinchi');
$spy->error('ikkinchi');
// Assert: yozilgan ma'lumotni KEYIN tekshiramiz (mock'da OLDIN belgilanardi)
$this->assertCount(2, $spy->records);
$this->assertSame('info', $spy->records[0]['level']);
$this->assertSame('ikkinchi', $spy->records[1]['message']);
}
}
Mock vs spy farqi falsafiy: mock β "kelajakka qaragan" (expects oldindan), spy β "o'tmishga qaragan" (records keyin). Spy testning Assert qismini ko'rinarli qiladi (AAA buzilmaydi), mock esa Arrange qismida tekshiruvni yashiradi. Ko'p jamoa shu sabab spy'ni afzal ko'radi.
Fake β ishlaydigan soddalashtirilgan implementatsiya¶
Eng "kuchli" double β fake: u haqiqiy mantiqqa ega, lekin yengillashtirilgan. Klassik misol β in-memory repository (21-bob repository naqshining test versiyasi):
<?php
declare(strict_types=1);
namespace App;
interface UserRepository
{
public function save(User $user): void;
public function findById(int $id): ?User;
public function findByEmail(string $email): ?User;
}
/** Ishlaydigan, soddalashtirilgan repository β bu FAKE (DB o'rniga massiv). */
final class InMemoryUserRepository implements UserRepository
{
/** @var array<int, User> */
private array $byId = [];
public function save(User $user): void
{
$this->byId[$user->id] = $user;
}
public function findById(int $id): ?User
{
return $this->byId[$id] ?? null;
}
public function findByEmail(string $email): ?User
{
foreach ($this->byId as $user) {
if ($user->email === $email) {
return $user;
}
}
return null;
}
}
<?php
declare(strict_types=1);
namespace Tests;
use App\InMemoryUserRepository;
use App\User;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class FakeRepositoryTest extends TestCase
{
#[Test]
public function fake_repository_haqiqiy_xulqqa_ega(): void
{
// FAKE: ishlaydigan in-memory implementatsiya
$repo = new InMemoryUserRepository();
$repo->save(new User(1, 'oqil@example.com'));
$repo->save(new User(2, 'aziza@example.com'));
// Fake HAQIQIY mantiqqa ega -> chindan topadi/topmaydi
$this->assertSame('oqil@example.com', $repo->findById(1)?->email);
$this->assertSame(2, $repo->findByEmail('aziza@example.com')?->id);
$this->assertNull($repo->findById(999));
}
}
Fake'ning afzalligi: u stub minglab willReturn yozishdan qutqaradi. save keyin findById haqiqatan ishlaydi β chunki fake'da real mantiq bor. Murakkab ssenariylar (ko'p save/find/delete ketma-ketligi)da fake stub'dan ancha o'qiladigan. Kamchiligi β fakeni o'zingiz yozasiz va saqlaysiz, demak unga ham ishonch kerak (ba'zida fake'ning o'ziga test yoziladi: "fake haqiqiy repository bilan bir xil shartnomaga bo'ysunadimi" β contract test).
Ko'prik β 21-bob. O'sha bobda Repository interfeysini va uning DB implementatsiyasini qurgansiz.
InMemoryUserRepositoryβ aynan o'sha interfeysning test uchun versiyasi. Service'ni testlashda haqiqiy DB o'rniga shu fake'ni in'ektsiya qilasiz β test millisekundlarda ishlaydi va izolyatsiyalangan bo'ladi.
Qachon MOCK QILMASLIK kerak: London vs Chicago¶
Yangi boshlovchilar ko'pincha hamma narsani mock qiladi. Bu xato. Test double β vosita, har joyga emas. Bu mavzu testlash falsafasidagi ikki maktab bilan bog'liq:
Chicago maktabi (classicist, "Detroit")¶
- Haqiqiy obyektlarni afzal ko'radi; double'ni faqat "noqulay" bog'liqlik (DB, tarmoq, vaqt, tasodif) uchun ishlatadi.
- Tekshiruv holat-asosli (state-based): "amaldan keyin natija to'g'rimi?"
- Misol:
Money::add()ni test qilganda hech narsa mock qilmaymiz βMoneytoza, bog'liqliksiz.CheckoutServiceni test qilganda faqatPaymentGateway(tarmoq) vaLogger(I/O) double bo'ladi.
London maktabi (mockist, "outside-in")¶
- Sinfning hamma bog'liqligini mock qiladi; sinfni "puxta izolyatsiyada" sinaydi.
- Tekshiruv xulq-asosli (interaction-based): "to'g'ri hamkorlar to'g'ri tartibda chaqirildimi?"
- Dizaynni "tashqaridan ichkariga" haydaydi: avval yuqori sinf, uning hamkorlarini mock bilan kashf etib, keyin ularni implementatsiya qiladi.
Qachon qaysi? Amaliy qoidalar¶
Mock QILMANG:
- Value object va toza mantiqni (
Money,Email, sof funksiya) β ular tez, deterministik, bog'liqliksiz. Haqiqiysini ishlating. - O'zingiz egasi bo'lmagan tipni ("Don't mock what you don't own") β masalan Guzzle yoki PDO ni to'g'ridan-to'g'ri mock qilish mo'rt: ularning ichki API'si o'zgarsa, mock'ingiz yolg'on yashil bo'ladi. O'rniga o'z interfeysingizga (
PaymentGateway) o'rab, shuni mock qiling. - Data structure / DTO (21-bob) β ular shunchaki ma'lumot, mock'lash ma'nosiz.
Mock QILING:
- Yon ta'sirli I/O: to'lov shlyuzi, email yuborish, tashqi HTTP, fayl tizimi β testda ularni haqiqatan bajarib bo'lmaydi.
- Side-effect muhim bo'lganda: "to'lovsiz email yuborilmasligi kerak" β bu o'zaro ta'sir, holat emas, mock kerak.
Ekspert nuqtai nazari: ko'pchilik tajribali muhandis classicist'ga yaqin turadi β kamroq mock, ko'proq haqiqiy obyekt va fake. Sabab: over-mocking testni kodning ichki tuzilishiga bog'lab qo'yadi. Agar testingiz "
a()keyinb()keyinc()chaqirildi" deb tekshirsa, siz implementatsiyani test qilyapsiz, xulqni emas β refactoring (b va c ni birlashtirish) testni buzadi, garchi xulq o'zgarmasa ham. Bu eng yomon test smell: mo'rt test (quyida).
Test smells: yomon testni qanday taniydigan¶
Yomon test β yo'qligidan ham battar: u noto'g'ri ishonch beradi yoki rivojlanishni sekinlashtiradi. Asosiy "hidlar":
1. Mo'rt (fragile) test¶
Implementatsiya o'zgarganda β xulq o'zgarmasa ham β yiqiladigan test. Sababi odatda over-mocking (ichki chaqiruvlarni tekshirish) yoki tashqi tafsilotga bog'lanish.
// β MO'RT: ichki metod chaqiruvlar tartibini tekshiradi
$mock->expects($this->once())->method('validateStep1');
$mock->expects($this->once())->method('validateStep2');
// validateStep1+2 ni bitta validate() ga birlashtirsangiz -> test yiqiladi
// (garchi tashqi xulq o'zgarmasa ham)
// β
MUSTAHKAM: faqat natijani tekshir
$this->assertTrue($validator->isValid($input));
2. Sekin (slow) test¶
Har bir test sekundlarda ishlasa β to'plam daqiqalarga cho'ziladi, dasturchi uni o'tkazib yuboradi. Sabab: haqiqiy DB/tarmoq/sleep(). Yechim: I/O ni fake/stub bilan almashtir, sekin testlarni #[Group('slow')] ga ajrat va ularni faqat CI'da ishga tushir.
3. Mystery guest (sirli mehmon)¶
Test o'zidan tashqaridagi yashirin holatga bog'liq β tashqi fayl, umumiy DB yozuvi, oldingi test qoldirgan holat. Testni o'qib, nima sodir bo'layotganini tushunib bo'lmaydi, chunki "mehmon" ko'rinmaydi.
// β MYSTERY GUEST: bu fayl qayerdan keldi? ichida nima bor?
$data = json_decode(file_get_contents(__DIR__ . '/fixtures/users.json'), true);
// β
Kerakli ma'lumotni test ICHIDA, ko'rinadigan qilib qur (Arrange)
$user = new User(1, 'oqil@example.com');
4. Bog'liq testlar (interdependent tests)¶
Testlar ma'lum tartibda ishlashga tayanadi: biri global holat qoldiradi, ikkinchisi unga muhtoj. Bittasini alohida ishga tushirsangiz β yiqiladi. Bu izolyatsiya buzilishi: har test mustaqil va istalgan tartibda ishlashi shart.
// β Bog'liq: $this->order static yoki test orasida saqlanadi
// Test A buyurtma yaratadi, Test B unga ishonadi -> alohida ishlasa yiqiladi
// β
setUp() har testdan oldin TOZA holat beradi -> mustaqillik
protected function setUp(): void { $this->order = new Order('id', 'USD'); }
Boshqa hidlar: assertion roulette (bir testda o'nlab xabardsiz assert β qaysi biri yiqildi noma'lum), mantiq testda (if/foreach test ichida β testning o'zi buggy bo'lishi mumkin), eager test (bir testda butun sinfni sinash).
Oltin qoida: test F.I.R.S.T. bo'lsin β Fast (tez), Isolated (izolyatsiyalangan), Repeatable (takrorlanuvchi, deterministik), Self-validating (o'zi yashil/qizil deydi, qo'lda tekshirish kerak emas), Timely (kod bilan birga, kech emas).
Hammasini ishga tushirish (yashil natija)¶
Yuqoridagi barcha sinflarni tests/ ga joylab, ishga tushiramiz:
--testdox β testlarni inson o'qiydigan jumlalar sifatida ko'rsatadi. Bu mashinada (PHP 8.4.0 + PHPUnit 12.5.29) haqiqiy chiqish:
PHPUnit 12.5.29 by Sebastian Bergmann and contributors.
Runtime: PHP 8.4.0
Configuration: phpunit.xml
............... 15 / 15 (100%)
Time: 00:00.024, Memory: 8.00 MB
Checkout Service (Tests\CheckoutService)
β Stub tayyor javob qaytaradi
β Mock kutilgan ozaro tasirni tekshiradi
β Tolov muvaffaqiyatsiz bolsa xato loglanadi
β Bosh buyurtma rad etiladi
Money (Tests\Money)
β Ikki summa qoshiladi
β AssertSame va assertEquals farqi
β Manfiy summa xato tashlaydi
β Mos kelmagan valyuta xato
β Turli summalar qoshiladi with nol + nol
β Turli summalar qoshiladi with oddiy
β Turli summalar qoshiladi with katta
...
OK (15 tests, 30 assertions)
OK (15 tests, 30 assertions) β hammasi yashil. Foydali bayroqlar:
php vendor/bin/phpunit --filter DependsLifecycle # nom bo'yicha tanlash
php vendor/bin/phpunit --group arifmetika # #[Group] bo'yicha
php vendor/bin/phpunit --stop-on-failure # birinchi yiqilishda to'xta
php vendor/bin/phpunit --testdox # o'qiladigan jumlalar
Coverage (qamrov) β illustrativ¶
Test qamrovi (code coverage) β testlar kodning necha foizini ishga tushirganini o'lchaydi. Bu metrikani olish uchun PHP'ga coverage drayveri kerak: Xdebug yoki PCOV.
Halol eslatma: bu mashinada Xdebug ham, PCOV ham o'rnatilmagan, shuning uchun coverage'ni haqiqatan ishga tushirib bo'lmaydi.
--coverage-textchaqirsak, PHPUnit aniq shunday deydi:Quyidagi
phpunit.xmlsozlamasi va kutilgan chiqish β ko'rsatuv uchun (sizning muhitingizda Xdebug/PCOV o'rnatilgan bo'lsa ishlaydi):
<!-- phpunit.xml ichida coverage hisobotini yoqish -->
<coverage>
<report>
<text outputFile="php://stdout" showOnlySummary="true"/>
<html outputDirectory="coverage-html"/>
</report>
</coverage>
Kutilgan chiqish (illustrativ):
Diqqat β coverage tuzog'i. 100% qamrov bug yo'qligini anglatmaydi. Qamrov faqat qator ishga tushganini o'lchaydi, to'g'ri tekshirilganini emas.
assertyozmasdan ham qatorni "qoplash" mumkin. Qamrovning sifatini o'lchaydigan haqiqiy metrika β mutation testing (Infection), uni 23-bobda ko'ramiz (u ham xdebug/pcov talab qiladi, shuning uchun illustrativ bo'ladi).
Sifat darvozasi: testning CI'dagi o'rni¶
Testlar β sifat darvozasining bir bo'lagi. To'liq darvoza odatda ketma-ket: lint β static analysis β test:
php vendor/bin/php-cs-fixer fix --dry-run # 1. uslub (PSR-12) -> 10-bob
php vendor/bin/phpstan analyse src tests # 2. statik tahlil -> keyingi boblar
php vendor/bin/phpunit # 3. testlar -> shu bob
Birinchi qadam yiqilsa, keyingisi ishlamaydi β "fail fast". Bu darvoza har pull requestda avtomatik ishlashi kerak.
Ko'prik β CI mexanikasi. GitHub Actions workflow yozish, matritsa (bir nechta PHP versiyasi), kesh va artefaktlar β bularning mexanikasi sizning Git va GitHub kitobingizda chuqur yoritilgan. Bu yerda biz faqat PHP sifat-darvozasining mazmunini (qaysi vositalar, qaysi tartibda) ko'rsatdik β uni o'sha workflow ichidagi qadamlarga joylashtirasiz. Ekspert qoidasi: lokal
composer testskripti va CI bir xil buyruqlarni ishlatsin, "menda ishlaydi-ku" muammosini yo'qotish uchun.
composer.json ga qulay skript qo'shing:
{
"scripts": {
"test": "phpunit",
"test:cov": "XDEBUG_MODE=coverage phpunit --coverage-text",
"check": [
"@php vendor/bin/php-cs-fixer fix --dry-run",
"@php vendor/bin/phpstan analyse",
"@php vendor/bin/phpunit"
]
}
}
Endi composer check β butun sifat darvozasini bir buyruqda ishga tushiradi.
Xulosa¶
Bu bobda test yozishni mexanikadan muhandislik faniga ko'tardik:
- Nega test β uchta ekspert sabab: regressiya himoyasi, refactoring ishonchi va eng nozigi β dizaynga bosim (testlanmaslik = yomon dizayn signali).
- AAA har testning grammatikasi; test piramidasi sog'lom taqsimot (ko'p unit), "muzqaymoq konus" esa anti-pattern.
- PHPUnit: zamonaviy atributlar (
#[Test]/#[DataProvider]/#[Depends]/#[Group]),setUp/tearDownhayot tsikli, va eng muhim βassertSame(standart, tip-aniq) vsassertEquals(==, ehtiyot). - Test doubles 5 turi β dummy (to'ldiruvchi), stub (tayyor javob), spy (yozib boradi), mock (o'zaro ta'sirni tekshiradi), fake (ishlaydigan soddalashtirilgan).
createStub/createMockvaexpects()->method()->with()->willReturn(). - Qachon mock qilmaslik β classicist (haqiqiy obyekt afzal) vs mockist; value object/toza mantiqni mock qilmang, faqat I/O va muhim side-effectni.
- Test smells β mo'rt, sekin, mystery guest, bog'liq testlar; F.I.R.S.T. prinsipi.
Hamma test sinflari haqiqiy PHP 8.4 + PHPUnit 12.5 bilan ishlatildi: 15 test, 30 assertion, yashil. Coverage va mutation testing xdebug/pcov talab qiladi β keyingi bobda ularni Pest va integration testlar bilan birga chuqur ko'ramiz.
Keyingi qadam β 23-bob: Pest (yangi, ifodali sintaksis), real
:memory:SQLite bilan integration testlar (repository darajasida), va sifatni o'lchaydigan ikki vosita β coverage va mutation testing (Infection) β illustrativ konfiguratsiya bilan.
Mashqlar¶
Oson¶
Moneysinfigasubtract(Money $other): selfmetodi qo'shing (natija manfiy bo'lsa istisno tashlasin). UngaassertSamebilan ikkita test yozing: oddiy ayirish va manfiy natija (expectException).assertSamevaassertEqualsfarqini ko'rsatadigan test yozing:assertEquals('5', 5)yashil, lekinassertSame('5', 5)qizil ekanini izohli kommentlar bilan tushuntiring.MoneyTest::summaProviderga yana ikkita holat (yield) qo'shing va data provider bilan ishlatilganini--testdoxchiqishida tasdiqlang.
O'rta¶
LoggeruchunSpyLoggero'rniga mock yozib,CheckoutServicemuvaffaqiyatli to'lovdainfo()ni aynan ikki marta chaqirishini tasdiqlang (expects($this->exactly(2))).- Yangi
NotificationServicesinfi yarating: uMailerinterfeysiga bog'liq vanotify(User $u)chaqirilgandaMailer::send()ni chaqiradi. Avval stub bilan (natija), keyin mock bilan (send aynan bir marta, to'g'ri email bilan chaqirilganini) test yozing. Ikki yondashuvni izohda solishtiring. InMemoryUserRepositorygadelete(int $id): voidqo'shing va fake bilan to'liq ssenariy testlang: save β find (topiladi) β delete β find (null). Nega bu yerda fake stub'dan o'qiladigan ekanini bir jumlada yozing.
Qiyin¶
setUp()da har testdan oldin uchta foydalanuvchiliInMemoryUserRepositoryquring. So'ng "bog'liq testlar" anti-patternini ataylab yarating (bir test foydalanuvchi qo'shadi, ikkinchisi unga ishonadi), uni ishga tushirib ko'ring, keyinsetUpizolyatsiyasi bilan tuzating. Farqni izohlang.CheckoutServiceni "London uslubida" (hamma bog'liqlik mock) va "Chicago uslubida" (faqat I/O mock,Order/Moneyhaqiqiy) test qiling. Qaysi biri refactoring (masalantotal()ichini o'zgartirish)da mo'rtroq β sinab ko'ring va xulosa yozing.phpunit.xmlga coverage konfiguratsiyasini qo'shing vaXDEBUG_MODE=coveragebilan ishga tushiring. Agar muhitingizda Xdebug bo'lmasa, "No code coverage driver available" xabarini ko'rsating va Xdebug'ni o'rnatish qadamlarini hujjatlang. Coverage 100% bo'lsa ham bug qolishi mumkinligini ko'rsatadigan bitta misol o'ylab toping.
Yechim β 1
// Money sinfi ichidagi yangi metod:
public function subtract(Money $other): self
{
if ($this->currency !== $other->currency) {
throw new \InvalidArgumentException('Valyutalar mos emas');
}
// natija manfiy bo'lsa, konstruktor o'zi istisno tashlaydi -> yangi tekshiruv shart emas
return new self($this->amount - $other->amount, $this->currency);
}
// MoneySubtractTest sinfi ichidagi metodlar:
#[Test]
public function ayirish_ishlaydi(): void
{
$r = (new Money(1000, 'USD'))->subtract(new Money(300, 'USD'));
$this->assertSame(700, $r->amount);
}
#[Test]
public function manfiy_natija_xato(): void
{
$this->expectException(\InvalidArgumentException::class);
(new Money(100, 'USD'))->subtract(new Money(500, 'USD')); // 100-500 < 0
}
Diqqat: manfiy natija tekshiruvini Money konstruktoriga topshirdik β DRY: validatsiya bir joyda.
Yechim β 2
<?php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class SameVsEqualsTest extends TestCase
{
#[Test]
public function same_va_equals_farqi(): void
{
// assertEquals -> == semantikasi -> tip moslashtiriladi -> '5' == 5 -> YASHIL
$this->assertEquals('5', 5);
// assertSame -> === semantikasi -> tip ham teng bo'lsin -> string('5') !== int(5)
// Quyidagi qator QIZIL bo'lardi (ataylab izohga olindi):
// $this->assertSame('5', 5); // β FAIL: Failed asserting that 5 is identical to '5'
// To'g'ri ishlatish: assertSame ayni tip bilan
$this->assertSame(5, 2 + 3); // int 5 === int 5 -> YASHIL
}
}
Xulosa: assertEquals tip xatosini yashiradi ('5' o'rniga 5 qaytsa ham yashil), assertSame esa uni ushlaydi. Standart holatda assertSame ishlating.
Yechim β 3
// MoneyTest sinfi ichidagi data provider metodi:
/** @return iterable<string, array{int, int, int}> */
public static function summaProvider(): iterable
{
yield 'nol + nol' => [0, 0, 0];
yield 'oddiy' => [100, 200, 300];
yield 'katta' => [999_999, 1, 1_000_000];
yield 'bir tomon nol' => [0, 777, 777]; // yangi
yield 'teng summalar' => [250, 250, 500]; // yangi
}
--testdox chiqishida har bir yield alohida qator bo'ladi:
Kalitlar ('bir tomon nol') test nomida ko'rinadi β biri yiqilsa, qaysi ma'lumot ayblini darrov bilamiz.
Yechim β 4
<?php
declare(strict_types=1);
namespace Tests;
use App\CheckoutService;
use App\Logger;
use App\Money;
use App\Order;
use App\PaymentGateway;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class CheckoutInfoCountTest extends TestCase
{
#[Test]
public function muvaffaqiyatli_tolovda_info_ikki_marta(): void
{
$gateway = $this->createStub(PaymentGateway::class);
$gateway->method('charge')->willReturn('tx-1');
// CheckoutService muvaffaqiyatli oqimda info() ni 2 marta chaqiradi:
// "Checkout boshlandi" + "To'lov muvaffaqiyatli"
$logger = $this->createMock(Logger::class);
$logger->expects($this->exactly(2))->method('info');
$logger->expects($this->never())->method('error'); // xato yo'q -> error chaqirilmaydi
$order = new Order('o-1', 'USD');
$order->addLine('Kitob', new Money(2000, 'USD'));
(new CheckoutService($gateway, $logger))->checkout($order, 'tok');
}
}
$this->never() β kuchli tekshiruv: "muvaffaqiyatli oqimda error umuman chaqirilmasligi kerak". Bu side-effect testi β mock'ning klassik o'rni.
Yechim β 5
<?php
declare(strict_types=1);
namespace App {
interface Mailer
{
public function send(string $to, string $subject, string $body): void;
}
final class NotificationService
{
public function __construct(private readonly Mailer $mailer) {}
public function notify(User $user): void
{
$this->mailer->send($user->email, 'Salom', 'Xush kelibsiz!');
}
}
}
namespace Tests {
use App\Mailer;
use App\NotificationService;
use App\User;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class NotificationServiceTest extends TestCase
{
// STUB yondashuvi (natija) β send() void qaytaradi, tekshiradigan natija yo'q,
// shuning uchun stub bu yerda kam ma'noli. Faqat "yiqilmadi"ni ko'rsatadi:
#[Test]
public function stub_bilan(): void
{
$mailer = $this->createStub(Mailer::class); // send() hech narsa qilmaydi
(new NotificationService($mailer))->notify(new User(1, 'a@b.com'));
$this->expectNotToPerformAssertions(); // void -> tekshiriladigan natija yo'q
}
// MOCK yondashuvi (o'zaro ta'sir) β bu yerda TO'G'RI tanlov:
#[Test]
public function mock_bilan(): void
{
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())
->method('send')
->with('a@b.com', 'Salom', 'Xush kelibsiz!'); // aynan shu argumentlar
(new NotificationService($mailer))->notify(new User(1, 'a@b.com'));
}
}
}
Solishtirish: send() void β uning butun maqsadi side-effect (email yuborish). Bunday holatda mock to'g'ri (o'zaro ta'sirni tekshiradi); stub esa deyarli foydasiz, chunki tekshiradigan qaytma qiymat yo'q.
Yechim β 6
// InMemoryUserRepository sinfi ichidagi yangi metod:
public function delete(int $id): void
{
unset($this->byId[$id]);
}
// FakeLifecycleTest sinfi ichidagi metod:
#[Test]
public function toliq_hayot_tsikli(): void
{
$repo = new InMemoryUserRepository();
$repo->save(new User(1, 'oqil@example.com'));
$this->assertNotNull($repo->findById(1)); // topiladi
$repo->delete(1);
$this->assertNull($repo->findById(1)); // o'chgandan keyin topilmaydi
}
Nega fake yaxshiroq: bu ssenariyda saveβfindβdeleteβfind ketma-ketligi holat o'zgarishiga bog'liq. Stub bilan har findById chaqiruvi uchun alohida willReturn/willReturnOnConsecutiveCalls yozish kerak bo'lardi β mo'rt va o'qib bo'lmas. Fake real mantiqqa ega, shuning uchun ketma-ketlik tabiiy ishlaydi.
Yechim β 7
<?php
declare(strict_types=1);
namespace Tests;
use App\InMemoryUserRepository;
use App\User;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class IzolyatsiyaTest extends TestCase
{
// β BOG'LIQ testlar (anti-pattern) β sinf darajasidagi xususiyatga tayanadi:
private InMemoryUserRepository $repo;
protected function setUp(): void
{
$this->repo = new InMemoryUserRepository();
$this->repo->save(new User(1, 'a@b.com'));
$this->repo->save(new User(2, 'c@d.com'));
$this->repo->save(new User(3, 'e@f.com'));
}
// PROBLEMA: agar quyidagi test save qilsa va keyingi test unga ishonsa,
// alohida ishga tushirilganda yiqiladi. Lekin setUp() HAR testdan oldin
// ishlagani uchun TOZA holat beradi -> aslida izolyatsiya saqlanadi.
// β
TUZATILGAN β har test mustaqil, setUp izolyatsiyani kafolatlaydi:
#[Test]
public function uchta_foydalanuvchi_bor(): void
{
$this->assertNotNull($this->repo->findById(2));
}
#[Test]
public function tortinchi_qoshilsa_oldingi_testga_tasir_qilmaydi(): void
{
$this->repo->save(new User(4, 'g@h.com'));
$this->assertNotNull($this->repo->findById(4));
// Bu o'zgarish KEYINGI testga oqib o'tmaydi, chunki setUp() yangi repo yaratadi
}
}
Kalit dars: setUp() har test uchun yangi $this->repo yaratadi (sinf maydoni qayta tayinlanadi), shuning uchun bir testning save'i boshqasiga ko'rinmaydi. Agar static maydon yoki setUpBeforeClass ishlatsangiz β izolyatsiya buziladi va testlar tartibga bog'liq bo'lib qoladi.
Yechim β 8
London uslubi (hamma narsa double β Order ham double):
Diqqat β
finalklassni dublyor qilib bo'lmaydi. BizningOrder(vaMoney)final(kitob konvensiyasi). PHPUnit dublyorni klassdan meros olib yasaydi,finalesa merosni taqiqlaydi β shuning uchun$this->createMock(Order::class)aniqClassIsFinalException: Class "App\Order" is declared "final" and cannot be doubledxatosini tashlaydi va test ishlamaydi. London uslubi ayni shu sabab sizni interfeysga ("port") suyanishga undaydi: kollaboratorningOrderInterfaceqobig'ini mock qiling, konkretfinalentityni emas. Bu β "outside-in dizaynni haydaydi" tamoyilining amaliy ko'rinishi.
<?php
declare(strict_types=1);
namespace App {
// London uslubi konkret `final` Order o'rniga uning interfeysiga suyanadi.
// (`final` klassni createMock/createStub dublyor qila olmaydi.)
interface OrderInterface
{
public function id(): string;
public function lineCount(): int;
public function total(): Money;
}
}
namespace Tests {
use App\CheckoutService;
use App\Logger;
use App\Money;
use App\OrderInterface;
use App\PaymentGateway;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class LondonUslubiTest extends TestCase
{
#[Test]
public function london_uslubi(): void
{
// $order'da expects() yo'q β faqat tayyor javob -> STUB (mock emas).
// PHPUnit 12.5 ham expects()siz mock uchun "stub ishlating" ogohlantiradi.
$order = $this->createStub(OrderInterface::class);
$order->method('lineCount')->willReturn(1);
$order->method('total')->willReturn(new Money(2500, 'USD'));
$order->method('id')->willReturn('o-1'); // haqiqiy getter, `__get` emas
$gateway = $this->createMock(PaymentGateway::class);
$gateway->expects($this->once())->method('charge')->willReturn('tx');
$logger = $this->createStub(Logger::class);
// CheckoutService endi OrderInterface'ga bog'liq (Order o'rniga) β
// aynan shu "interfeysga dasturlash" London uslubining tabiiy mevasi.
(new CheckoutService($gateway, $logger))->checkout($order, 'tok');
}
}
}
Chicago uslubi (faqat I/O mock, Order/Money haqiqiy):
<?php
declare(strict_types=1);
namespace Tests;
use App\CheckoutService;
use App\Logger;
use App\Money;
use App\Order;
use App\PaymentGateway;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ChicagoUslubiTest extends TestCase
{
#[Test]
public function chicago_uslubi(): void
{
$order = new Order('o-1', 'USD'); // HAQIQIY
$order->addLine('Kitob', new Money(2500, 'USD')); // HAQIQIY Money
$gateway = $this->createMock(PaymentGateway::class); // faqat I/O mock
$gateway->expects($this->once())
->method('charge')
->with($this->callback(fn (Money $m) => $m->amount === 2500))
->willReturn('tx');
$logger = $this->createStub(Logger::class);
(new CheckoutService($gateway, $logger))->checkout($order, 'tok');
}
}
Xulosa: London uslubi mo'rtroq. Order::total() ichini o'zgartirsangiz (masalan chegirma qo'shsangiz), London testidagi willReturn(new Money(2500,...)) eskirib qoladi β test yashil bo'ladi-yu, lekin haqiqiy total() boshqacha hisoblashi mumkin (yolg'on yashil). Chicago testi esa haqiqiy Order::total() ni ishlatadi, shuning uchun hisoblash mantig'idagi xatoni ushlaydi. Shu sabab toza mantiq (Order, Money) ni mock qilmaslik kerak.
E'tibor bering: London uslubi Orderni dublyor qilish uchun bizni qo'shimcha OrderInterface va id() getter qo'shishga majbur qildi (chunki konkret final Orderni dublyor qilib bo'lmaydi) β ya'ni test ehtiyoji domen dizayniga bosim o'tkazdi. Chicago esa hech qanday qo'shimcha qatlamsiz, haqiqiy Order bilan ishlayverdi. Domen entity'lari (value object, agregat) toza mantiq bo'lsa β ularni Chicago uslubida haqiqiy ishlatish ham soddaroq, ham xatoga sezgirroq.
Yechim β 9
phpunit.xml ga coverage bloki:
<coverage>
<report>
<text outputFile="php://stdout" showOnlySummary="true"/>
<html outputDirectory="coverage-html"/>
</report>
</coverage>
Ishga tushirish:
Xdebug yo'q muhitda (shu mashina kabi) chiqish:
Xdebug o'rnatish (Windows, Herd/standalone PHP uchun):
- https://xdebug.org/wizard ga
php -ichiqishini joylab, mos.dllni yuklab oling. php.iniga:zend_extension=xdebugvaxdebug.mode=coverage(yokiXDEBUG_MODEenv).php -mdaxdebugko'rinishini tasdiqlang.
Yengilroq alternativa β PCOV (faqat coverage uchun, Xdebug'dan tez):
100% qamrov bug yashiradigan misol:
// Tekshirilayotgan sinf ichidagi metod (ataylab buggy):
public function bol(int $a, int $b): int
{
return $a + $b; // β aslida ayirish bo'lishi kerak edi
}
// Test sinfi ichidagi metod β 100% qator qamrovi beradi, lekin BUGNI ko'rmaydi:
#[Test]
public function test_bol(): void
{
$this->bol(2, 2); // qator ishga tushdi -> qamrov 100%
// ... lekin natijani ASSERT QILMADIK -> 2+2 ham, 2-2 ham "qoplangan"
}
Bu qator qamrovning chegarasini ko'rsatadi: u faqat "ishga tushdimi" deydi, "to'g'rimi" demaydi. Buni mutation testing (Infection) o'lchaydi β u kodga ataylab "mutatsiya" (+ ni - ga) kiritib, testlar buni ushlaydimi deb sinaydi. 23-bobda ko'ramiz.
β¬ οΈ Oldingi: 21 β Taktik dizayn: Repository, Service, DTO Β· π README Β· Keyingi: 23 β Pest, integratsiya, coverage va mutation β‘οΈ