13-bo‘lim
TDD - test asosida ishlab chiqish
Qizil-yashil-refaktor tsikli, to'liq amaliy misol, TDD ning afzalliklari va cheklovlari.
Ushbu bo‘lim mundarijasi
- Qizil-yashil-refaktor
- To'liq misol
- 1-tsikl
- 2-tsikl
- 3-tsikl
- 4-tsikl - yangi talab
- To'liq test to'plami
- TDD ning afzalliklari
- TDD qachon mos kelmaydi?
- Amaliy yondashuv
- Xatolarni TDD bilan tuzatish
- Mavjud loyihaga TDD ni kiritish
- BDD - xatti-harakatga yo'naltirilgan
- TDD anti-naqshlari
- Statistika
- Xulosa
TDD (Test-Driven Development) - kodni testdan keyin emas, testdan oldin yozish usuli.
Qizil-yashil-refaktor #
- O'tmaydigan test yozmasdan ishlab chiqarish kodini yozmang
- Test o'tmasligi uchun yetarlicha test yozing
- Testni o'tkazish uchun yetarlicha kod yozing
To'liq misol #
Vazifa: parol kuchini baholovchi klass yozamiz.
1-tsikl #
Qizil:
final class ParolBaholovchiTest extends TestCase
{
public function test_bosh_parol_juda_zaif(): void
{
$baholovchi = new ParolBaholovchi();
$this->assertEquals(ParolKuchi::JudaZaif, $baholovchi->baholang(''));
}
}
Error: Class "ParolBaholovchi" not found
Yashil - eng oddiy yechim:
enum ParolKuchi
{
case JudaZaif;
case Zaif;
case Ortacha;
case Kuchli;
}
final class ParolBaholovchi
{
public function baholang(string $parol): ParolKuchi
{
return ParolKuchi::JudaZaif;
}
}
OK (1 test, 1 assertion)
Ha, va bu normal. TDD da siz eng oddiy ishlaydigan yechimni yozasiz.
Keyingi test bu yechimni yetarli emasligini ko'rsatadi va sizni yaxshilashga majbur qiladi.
Bu usul ortiqcha kod yozishning oldini oladi.
2-tsikl #
Qizil:
public function test_uzun_va_murakkab_parol_kuchli(): void
{
$baholovchi = new ParolBaholovchi();
$this->assertEquals(
ParolKuchi::Kuchli,
$baholovchi->baholang('Xy7#mQ2$pL9wZ')
);
}
Failed asserting that ParolKuchi::JudaZaif matches ParolKuchi::Kuchli
Yashil:
final class ParolBaholovchi
{
public function baholang(string $parol): ParolKuchi
{
if (strlen($parol) >= 12) {
return ParolKuchi::Kuchli;
}
return ParolKuchi::JudaZaif;
}
}
3-tsikl #
Qizil:
public function test_uzun_lekin_oddiy_parol_ortacha(): void
{
$baholovchi = new ParolBaholovchi();
$this->assertEquals(
ParolKuchi::Ortacha,
$baholovchi->baholang('parolparolparol')
);
}
Yashil:
final class ParolBaholovchi
{
public function baholang(string $parol): ParolKuchi
{
$ball = 0;
if (strlen($parol) >= 8) { $ball++; }
if (strlen($parol) >= 12) { $ball++; }
if (preg_match('/[a-z]/', $parol)) { $ball++; }
if (preg_match('/[A-Z]/', $parol)) { $ball++; }
if (preg_match('/[0-9]/', $parol)) { $ball++; }
if (preg_match('/[^a-zA-Z0-9]/', $parol)) { $ball++; }
return match (true) {
$ball >= 5 => ParolKuchi::Kuchli,
$ball >= 3 => ParolKuchi::Ortacha,
$ball >= 1 => ParolKuchi::Zaif,
default => ParolKuchi::JudaZaif,
};
}
}
Refaktor:
final class ParolBaholovchi
{
private const MINIMAL_UZUNLIK = 8;
private const YAXSHI_UZUNLIK = 12;
public function baholang(string $parol): ParolKuchi
{
return $this->ballgaKuch($this->ballHisoblang($parol));
}
private function ballHisoblang(string $parol): int
{
$tekshiruvlar = [
fn (string $p) => strlen($p) >= self::MINIMAL_UZUNLIK,
fn (string $p) => strlen($p) >= self::YAXSHI_UZUNLIK,
fn (string $p) => (bool) preg_match('/[a-z]/', $p),
fn (string $p) => (bool) preg_match('/[A-Z]/', $p),
fn (string $p) => (bool) preg_match('/[0-9]/', $p),
fn (string $p) => (bool) preg_match('/[^a-zA-Z0-9]/', $p),
];
return count(array_filter($tekshiruvlar, fn ($t) => $t($parol)));
}
private function ballgaKuch(int $ball): ParolKuchi
{
return match (true) {
$ball >= 5 => ParolKuchi::Kuchli,
$ball >= 3 => ParolKuchi::Ortacha,
$ball >= 1 => ParolKuchi::Zaif,
default => ParolKuchi::JudaZaif,
};
}
}
OK (3 tests, 3 assertions)
Testlar yashil bo'lgani uchun refaktoringdan keyin darhol tekshirish mumkin. Biror narsa buzilsa - bir zumda bilib olasiz.
Testsiz refaktoring - qorong'ida yurish.
4-tsikl - yangi talab #
public function test_keng_tarqalgan_parol_juda_zaif(): void
{
$baholovchi = new ParolBaholovchi(['parol123', 'qwerty', '12345678']);
$this->assertEquals(
ParolKuchi::JudaZaif,
$baholovchi->baholang('parol123')
);
}
final class ParolBaholovchi
{
public function __construct(private array $tarqalganParollar = []) {}
public function baholang(string $parol): ParolKuchi
{
if (in_array(strtolower($parol), $this->tarqalganParollar, true)) {
return ParolKuchi::JudaZaif;
}
return $this->ballgaKuch($this->ballHisoblang($parol));
}
// ...
}
To'liq test to'plami #
final class ParolBaholovchiTest extends TestCase
{
private ParolBaholovchi $baholovchi;
protected function setUp(): void
{
$this->baholovchi = new ParolBaholovchi(['parol123', 'qwerty']);
}
/**
* @dataProvider parollar
*/
public function test_parol_kuchi_togri_baholanadi(
string $parol,
ParolKuchi $kutilgan,
): void {
$this->assertEquals($kutilgan, $this->baholovchi->baholang($parol));
}
public static function parollar(): array
{
return [
'bosh' => ['', ParolKuchi::JudaZaif],
'tarqalgan' => ['parol123', ParolKuchi::JudaZaif],
'faqat kichik' => ['abcdefgh', ParolKuchi::Ortacha],
'kichik va son' => ['abcd1234', ParolKuchi::Ortacha],
'aralash uzun' => ['Xy7#mQ2$pL9wZ', ParolKuchi::Kuchli],
'qisqa aralash' => ['Ab1#', ParolKuchi::Ortacha],
'faqat son' => ['1234', ParolKuchi::Zaif],
];
}
}
TDD ning afzalliklari #
Testni oldin yozganingizda, siz klassni foydalanuvchi nuqtayi nazaridan ko'rasiz.
"Bu klassni qanday ishlatish qulay?" degan savol avval, "qanday amalga oshiraman?" degan savol keyin keladi.
Natijada interfeys tabiiy va qulay chiqadi.
TDD qachon mos kelmaydi? #
| Vaziyat | Sabab |
|---|---|
| Prototip, tajriba | Kod baribir tashlanadi |
| UI tafsilotlari | Testlash qiyin va foydasi kam |
| Notanish API ni o'rganish | Avval qanday ishlashini bilish kerak |
| Oddiy CRUD | Biznes mantiq yo'q |
| Ishlash optimizatsiyasi | Test emas, o'lchov kerak |
TDD murakkab mantiq uchun eng foydali: hisob-kitob, biznes qoidalari, algoritmlar, holat mashinalari.
Amaliy yondashuv #
// 1. Avval interfeysni o'ylang - testda uni "ishlatib" ko'ring
public function test_yetkazish_narxi_hisoblanadi(): void
{
$hisoblagich = new YetkazishHisoblagichi();
$narx = $hisoblagich->hisoblang(
vazn: 3.5,
masofa: 12,
usul: YetkazishUsuli::Kuryer,
);
$this->assertEquals(21_000, $narx->som());
}
Bu testni yozayotganda siz allaqachon qaror qabul qildingiz:
- Metod nomi
hisoblang - Nomlangan parametrlar ishlatiladi
- Usul -
enum, matn emas - Natija -
Pulobyekti,floatemas
Sizning kodingizni birinchi ishlatuvchi - test.
Agar testda kodni ishlatish noqulay bo'lsa, demak haqiqiy kodda ham noqulay bo'ladi.
Xatolarni TDD bilan tuzatish #
// 1. Xatoni takrorlaydigan test yozing
public function test_nol_masofada_xato_bermaydi(): void
{
$hisoblagich = new YetkazishHisoblagichi();
// Bu hozir DivisionByZeroError beradi
$narx = $hisoblagich->hisoblang(vazn: 1, masofa: 0, usul: YetkazishUsuli::Kuryer);
$this->assertEquals(15_000, $narx->som());
}
DivisionByZeroError: Division by zero
// 2. Tuzating
public function hisoblang(float $vazn, float $masofa, YetkazishUsuli $usul): Pul
{
if ($masofa < 0) {
throw new InvalidArgumentException('Masofa manfiy bo\'lolmaydi');
}
return $usul->asosiyNarx()->qoshing(
Pul::somdan($masofa * $usul->kilometrNarxi())
);
}
Bu regressiya testi deb ataladi.
Xato bir marta yuz bergan bo'lsa, u qaytishi mumkin. Test uning qaytmasligini kafolatlaydi.
Bu TDD ni loyihaga kiritishning eng oson yo'li: yangi kodga emas, xatolarga test yozishdan boshlang.
Mavjud loyihaga TDD ni kiritish #
1. Yangi kod uchun TDD dan boshlang
2. Xato tuzatishda avval test yozing
3. Refaktoring qilishdan oldin mavjud xatti-harakatni testlang
4. Eng muhim biznes mantiqni asta-sekin qoplang
5. Butun kodni bir vaqtda testlash - imkonsiz maqsad
Eski kodni tushunmasdan turib o'zgartirish kerak bo'lsa:
public function test_hozirgi_xatti_harakat(): void
{
$natija = $eskiKod->hisobla(100, 5);
// Nima chiqishini bilmayman - ishga tushirib ko'raman
$this->assertEquals(??, $natija);
}
Testni ishga tushiring, haqiqiy natijani ko'ring va uni yozib qo'ying.
Endi sizda xavfsizlik to'ri bor: refaktoring qilsangiz va natija o'zgarsa - darhol bilasiz.
BDD - xatti-harakatga yo'naltirilgan #
Xususiyat: Yetkazish narxini hisoblash
Ssenariy: Kuryer bilan yaqin masofaga
Nazarda tutilgan buyurtma vazni 2 kg
Va yetkazish masofasi 5 km
Qachonki kuryer usuli tanlansa
U holda yetkazish narxi 17500 so'm bo'ladi
Ssenariy: Bepul yetkazish chegarasi
Nazarda tutilgan buyurtma summasi 500000 so'm
Qachonki yetkazish hisoblansa
U holda yetkazish bepul bo'ladi
// tests/Behat/YetkazishKonteksti.php
final class YetkazishKonteksti implements Context
{
private float $vazn;
private float $masofa;
private Pul $natija;
/**
* @Given buyurtma vazni :vazn kg
*/
public function buyurtmaVazni(float $vazn): void
{
$this->vazn = $vazn;
}
/**
* @When :usul usuli tanlansa
*/
public function usulTanlansa(string $usul): void
{
$hisoblagich = new YetkazishHisoblagichi();
$this->natija = $hisoblagich->hisoblang(
$this->vazn, $this->masofa, YetkazishUsuli::from($usul)
);
}
/**
* @Then yetkazish narxi :narx so'm bo'ladi
*/
public function yetkazishNarxi(int $narx): void
{
Assert::assertEquals($narx, $this->natija->som());
}
}
Mijoz yoki biznes tahlilchi testlarni o'qiy olishi kerak bo'lsa.
Ssenariylar biznes tilida yozilgani uchun ular bir vaqtda:
- Talablar hujjati
- Avtomatik test
- Yetkazib berish dalili
Kamchiligi: qo'shimcha qatlam va qo'llab-quvvatlash xarajati. Faqat haqiqatan kerak bo'lganda ishlating.
TDD anti-naqshlari #
| Xato | Nima uchun yomon |
|---|---|
| Testni koddan keyin yozish | Bu TDD emas, oddiy testlash |
| Juda katta qadam | Test o'tmasa, sabab noaniq |
| Refaktoring bosqichini o'tkazib yuborish | Kod sifati yomonlashadi |
| Amalga oshirish tafsilotlarini testlash | Refaktoringda test buziladi |
| Qizil bosqichni ko'rmaslik | Test aslida hech nima tekshirmasligi mumkin |
| Testlarni sharhlab qo'yish | "Keyin tuzataman" - hech qachon |
public function test_bekor_qilinadi(): void
{
$buyurtma = new Buyurtma();
$buyurtma->bekorQiling();
// assert unutilgan!
}
Bu test har doim o'tadi - u hech nima tekshirmaydi.
Agar avval qizil bosqichni ko'rgan bo'lsangiz, bunday xato darhol seziladi.
Statistika #
Microsoft va IBM tadqiqotlariga ko'ra, TDD qo'llagan jamoalarda:
- Xatolar soni 40-90% kamaygan
- Dastlabki ishlab chiqish vaqti 15-35% oshgan
- Qo'llab-quvvatlash xarajati sezilarli kamaygan
Ya'ni: boshida sekinroq, keyin ancha tezroq.
Loyiha qanchalik uzoq yashasa, TDD shunchalik foydali.
- TDD bilan
RimRaqamlarikonvertorini yozing (1-3999). - Har bir tsiklda faqat bitta test qo'shing.
- Qizil bosqichni albatta ko'ring.
- Har uch tsikldan keyin refaktoring qiling.
- TDD bilan
FizzBuzzyozing. - TDD bilan savat chegirmasi hisoblagichini yozing.
- Mavjud loyihangizdagi xatoni toping va avval test yozing.
- Eski kod uchun xarakteristika testi yozing.
- Bitta xususiyat uchun Gherkin ssenariylarini yozing.
- TDD bilan yozilgan kodni oddiy usulda yozilgani bilan solishtiring.
Xulosa #
- TDD - testni koddan oldin yozish usuli.
- Tsikl: qizil (o'tmaydigan test) → yashil (eng oddiy yechim) → refaktor.
- Qadamlar kichik bo'lsin - tsikl 2-10 daqiqa.
- "Aldash" normal - keyingi test sizni yaxshilashga majbur qiladi.
- Eng katta foyda - dizaynga ta'sir: kod testlanadigan va qulay bo'ladi.
- Test - birinchi mijoz: testda noqulay bo'lsa, kodda ham noqulay.
- Har bir xato uchun regressiya testi yozing.
- Mavjud loyihada xatolardan boshlang, hamma narsani birdan qoplashga urinmang.
- Xarakteristika testlari eski kodni xavfsiz o'zgartirish imkonini beradi.
- TDD prototip, UI va oddiy CRUD uchun mos emas.
- Qizil bosqichni albatta ko'ring - aks holda test bo'sh bo'lishi mumkin.
Keyingi bo'limda kod tekshiruvi va sifat nazoratini ko'ramiz.
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.