13-bo‘lim

TDD - test asosida ishlab chiqish

Qizil-yashil-refaktor tsikli, to'liq amaliy misol, TDD ning afzalliklari va cheklovlari.

🕑 15 daqiqa o‘qish 📄 898 so‘z 👁 8 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Qizil-yashil-refaktor
  2. To'liq misol
  3. 1-tsikl
  4. 2-tsikl
  5. 3-tsikl
  6. 4-tsikl - yangi talab
  7. To'liq test to'plami
  8. TDD ning afzalliklari
  9. TDD qachon mos kelmaydi?
  10. Amaliy yondashuv
  11. Xatolarni TDD bilan tuzatish
  12. Mavjud loyihaga TDD ni kiritish
  13. BDD - xatti-harakatga yo'naltirilgan
  14. TDD anti-naqshlari
  15. Statistika
  16. Xulosa

TDD (Test-Driven Development) - kodni testdan keyin emas, testdan oldin yozish usuli.

Qizil-yashil-refaktor #

TDD tsikli 1. QIZIL O'tmaydigan test yozing 2. YASHIL Eng oddiy yechim bilan o'tkazing 3. REFAKTOR Kodni yaxshilang Kichik qadamlar tsikl 2-10 daqiqa Har bir tsiklda faqat bitta kichik imkoniyat qo'shiladi
Tsikl qanchalik qisqa bo'lsa, shunchalik yaxshi
Uchta qoida (Uncle Bob)
  1. O'tmaydigan test yozmasdan ishlab chiqarish kodini yozmang
  2. Test o'tmasligi uchun yetarlicha test yozing
  3. Testni o'tkazish uchun yetarlicha kod yozing

To'liq misol #

Vazifa: parol kuchini baholovchi klass yozamiz.

1-tsikl #

Qizil:

PHP
final class ParolBaholovchiTest extends TestCase
{
    public function test_bosh_parol_juda_zaif(): void
    {
        $baholovchi = new ParolBaholovchi();

        $this->assertEquals(ParolKuchi::JudaZaif, $baholovchi->baholang(''));
    }
}
Natija
Error: Class "ParolBaholovchi" not found

Yashil - eng oddiy yechim:

PHP
enum ParolKuchi
{
    case JudaZaif;
    case Zaif;
    case Ortacha;
    case Kuchli;
}

final class ParolBaholovchi
{
    public function baholang(string $parol): ParolKuchi
    {
        return ParolKuchi::JudaZaif;
    }
}
Natija
OK (1 test, 1 assertion)
"Bu aldash-ku!"

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:

PHP
public function test_uzun_va_murakkab_parol_kuchli(): void
{
    $baholovchi = new ParolBaholovchi();

    $this->assertEquals(
        ParolKuchi::Kuchli,
        $baholovchi->baholang('Xy7#mQ2$pL9wZ')
    );
}
Natija
Failed asserting that ParolKuchi::JudaZaif matches ParolKuchi::Kuchli

Yashil:

PHP
final class ParolBaholovchi
{
    public function baholang(string $parol): ParolKuchi
    {
        if (strlen($parol) >= 12) {
            return ParolKuchi::Kuchli;
        }

        return ParolKuchi::JudaZaif;
    }
}

3-tsikl #

Qizil:

PHP
public function test_uzun_lekin_oddiy_parol_ortacha(): void
{
    $baholovchi = new ParolBaholovchi();

    $this->assertEquals(
        ParolKuchi::Ortacha,
        $baholovchi->baholang('parolparolparol')
    );
}

Yashil:

PHP
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:

PHP
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,
        };
    }
}
Natija
OK (3 tests, 3 assertions)
Refaktoring xavfsiz

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 #

PHP
public function test_keng_tarqalgan_parol_juda_zaif(): void
{
    $baholovchi = new ParolBaholovchi(['parol123', 'qwerty', '12345678']);

    $this->assertEquals(
        ParolKuchi::JudaZaif,
        $baholovchi->baholang('parol123')
    );
}
PHP
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 #

PHP
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 #

TDD nima beradi? Dizaynga ta'siri • Kod avtomatik testlanadigan bo'ladi • Bog'liqliklar interfeys orqali • Klasslar kichik va aniq Jarayonga ta'siri • Talab avval aniqlanadi • Ortiqcha kod yozilmaydi • Tez fikr-mulohaza Sifatga ta'siri • Yuqori qoplama tabiiy chiqadi • Chekka holatlar o'ylanadi • Xato kam bo'ladi Qiyinchiliklari • O'rganish vaqt oladi • Boshida sekinroq • Hamma vazifaga mos emas
TDD - sifat vositasi, tezlik vositasi emas
Eng katta foyda - dizaynga ta'sir

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? #

TDD - universal yechim emas
VaziyatSabab
Prototip, tajribaKod baribir tashlanadi
UI tafsilotlariTestlash qiyin va foydasi kam
Notanish API ni o'rganishAvval qanday ishlashini bilish kerak
Oddiy CRUDBiznes mantiq yo'q
Ishlash optimizatsiyasiTest emas, o'lchov kerak

TDD murakkab mantiq uchun eng foydali: hisob-kitob, biznes qoidalari, algoritmlar, holat mashinalari.

Amaliy yondashuv #

PHP
// 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 - Pul obyekti, float emas
Test - birinchi mijoz

Sizning kodingizni birinchi ishlatuvchi - test.

Agar testda kodni ishlatish noqulay bo'lsa, demak haqiqiy kodda ham noqulay bo'ladi.

Xatolarni TDD bilan tuzatish #

PHP
// 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());
}
Natija
DivisionByZeroError: Division by zero
PHP
// 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())
    );
}
Har bir xato uchun test yozing

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 #

Natija
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
"Xarakteristika testlari"

Eski kodni tushunmasdan turib o'zgartirish kerak bo'lsa:

PHP
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 #

GHERKIN
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
PHP
// 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());
    }
}
BDD qachon foydali?

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 #

Umumiy xatolar
XatoNima uchun yomon
Testni koddan keyin yozishBu TDD emas, oddiy testlash
Juda katta qadamTest o'tmasa, sabab noaniq
Refaktoring bosqichini o'tkazib yuborishKod sifati yomonlashadi
Amalga oshirish tafsilotlarini testlashRefaktoringda test buziladi
Qizil bosqichni ko'rmaslikTest aslida hech nima tekshirmasligi mumkin
Testlarni sharhlab qo'yish"Keyin tuzataman" - hech qachon
Qizil bosqichni albatta ko'ring
PHP
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 #

Tadqiqotlar nima deydi?

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.

Amaliy topshiriq
  1. TDD bilan RimRaqamlari konvertorini yozing (1-3999).
  2. Har bir tsiklda faqat bitta test qo'shing.
  3. Qizil bosqichni albatta ko'ring.
  4. Har uch tsikldan keyin refaktoring qiling.
  5. TDD bilan FizzBuzz yozing.
  6. TDD bilan savat chegirmasi hisoblagichini yozing.
  7. Mavjud loyihangizdagi xatoni toping va avval test yozing.
  8. Eski kod uchun xarakteristika testi yozing.
  9. Bitta xususiyat uchun Gherkin ssenariylarini yozing.
  10. 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.

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.