Přejít k navigační liště

Zdroják » PHP » Testování v PHP: praktický příklad

Testování v PHP: praktický příklad

Články PHP, Různé

Dnes si na praktickém příkladu ukážeme, jak pokrýt třídu testy a následně se pustíme do refaktoringu podle pravidel test driven development.

Na základně vašich reakcí pod posledním dílem seriálu jsem se rozhodl vložit jeden mimořádný díl s praktickou ukázkou přínosu testování kódu. Ukážeme si, jak pokrýt testy jednoduchou třídu, a následně ji budeme refaktorovat podle požadavků pomyslného zadavatele. Refaktoring budeme provádět metodou test driven development. Předlohou budiž známý příklad od autora frameworku PHPUnit, Sebastiana Bergmanna, s názvem BankAccount, který si pro naše potřeby malinko upravíme. Předem bych ještě rád upozornil, že účelem příkladu je seznámit čtenáře s problematikou testování kódu, ne s principy dokonalého objektového návrhu. I proto jsou některé části příkladu značně zjednodušeny!

Zadání

Našim úkolem je napsat třídu pro správu bankovního účtu. Je požadováno rozhraní umožňující:

  • vložení částky na účet
  • výběr částky z účtu
  • zjištění zůstatku
  • získání seznamu pohybů (změn zůstatku) na účtu

Každý účet má jednoho majitele, který je reprezentován jménem a příjmením. Implementace musí také pamatovat na základní chybové stavy:

  • pokus o „vložení“ záporné částky
  • pokus o výběr částky, která přesahuje zůstatek

Na tyto chybové stavy implementace reaguje vyvoláním výjimky třídy InvalidArgumentException. Posledním požadavkem je, aby implementace pamatovala na udržování seznamu změn na účtu, např. pro budoucí sestavení výpisu z účtu. Pro zjednodušení bude účet pracovat pouze se dvěma desetinnými místy a další bez zaokrouhlení ignorovat.

Implementace

Naše prvotní implementace může vypadat zhruba takto:

class BankAccount
{
    protected $name;
    protected $surname;

    protected $balance = 0.0;
    protected $changes = array();

    /**
     * @param string $name Owner's name
     * @param string $surname Owner's surname
     */
    public function __construct($name, $surname)
    {
        $this->name = $name;
        $this->surname = $surname;
    }

    /**
     * @return string
     */
    public function getOwnerName()
    {
        return trim($this->name . " " . $this->surname);
    }

    /**
     * @param float $amount
     * @return float
     * @throws InvalidArgumentException
     */
    public function depositMoney($amount)
    {
        $amount = (double)sprintf("%.02f", (double)$amount);
        if ($amount <= 0) {
            throw new InvalidArgumentException(
                "Cannot deposit negative amount of money");
        }

        return $this->addAmount($amount,
            "deposit: " . sprintf("%.02f", $amount));
    }

    /**
     * @param float $amount
     * @return float
     * @throws InvalidArgumentException
     */
    public function withdrawMoney($amount)
    {
        $amount = (double)sprintf("%.02f", (double)$amount);
        if ($this->getBalance() < $amount) {
            throw new InvalidArgumentException("Insufficient balance");
        }

        return $this->subAmount($amount,
            "withdraw: " . sprintf("%.02f", $amount));
    }

    /**
     * @return float
     */
    public function getBalance()
    {
        return (double)sprintf("%.02f", $this->balance);
    }

    /**
     * @return array
     */
    public function getChanges()
    {
        return $this->changes;
    }

    /**
     * @param float $amount
     * @param string $reason
     * @return float
     */
    protected function addAmount($amount, $reason)
    {
        $this->balance += $amount;
        $this->changes[] = $reason;

        return $this->balance;
    }

    /**
     * @param float $amount
     * @param string $reason
     * @return float
     */
    protected function subAmount($amount, $reason)
    {
        $this->balance -= $amount;
        $this->changes[] = $reason;

        return $this->balance;
    }
}

Testování

Nyní přistoupíme k otestování. Prvotní implementaci pokryjeme sadou testů, která bude ověřovat základní funkčnost jednotlivých metod a bude také pamatovat na některé nestandardní vstupy, které popisuje zadání. Připravíme si testovací sadu, kterou pojmenujeme BankAccountTest a pomocí metody setUp si nastavíme fixture, což v našem případě bude pouze instance testované třídy: BankAccount

class BankAccountTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var BankAccount
     */
    protected $account;

    protected function setUp()
    {
        $this->account = new BankAccount("John", "Doe");
    }
}

Nezapomeňme na include souboru s testovanou třídou! Prvním test case bude ověření, zda se správně skládá jméno a příjmení majitele účtu. Jakkoli se může tento test case zdát triviální až zbytečný, může nám v budoucnu signalizovat problém, pokud se například rozhodneme pracovat i s prostředními jmény majitele, jeho akademickými tituly apod.

public function testGetOwnerName()
{
    $this->assertSame("John Doe", $this->account->getOwnerName());
}

Dále otestujeme korektní chování metody depositMoney pro vklad částky na účet, prozatím se standardními vstupy.

public function testDepositMoney()
{
    // check initial balance
    $this->assertSame(0.0, $this->account->getBalance());

    // deposit some money
    $this->account->depositMoney(100);
    $this->assertEquals(100, $this->account->getBalance());

    // deposit again
    $this->account->depositMoney(12.99);
    $this->assertSame(112.99, $this->account->getBalance());
}

Protože metoda depositMoney nesmí přijmout nulový nebo záporný vklad, otestujeme i tuto možnost. Pro tento test case použijeme anotaci @expectedException, o které bude řeč v příštím díle seriálu. Anotace vyžaduje aby v následujícím test case byla vyvolána výjimka uvedené třídy. Pokud k jejímu vyvolání nedojde, test case selže.

/**
 * @expectedException InvalidArgumentException
 */
public function testDepositeNegativeAmountOfMoney()
{
    $this->account->depositMoney(-1);
}

Stejný postup použijeme i v případě metody withdrawMoney, tedy výběr částky z účtu. Nejprve testujeme chování s korektními vstupy, potom nestandardní.

public function testWithdrawMoney()
{
    // deposit some money first
    $this->account->depositMoney(1000);

    $this->account->withdrawMoney(899.99);
    $this->assertSame(100.01, $this->account->getBalance());

    $this->account->withdrawMoney(10.01);
    $this->assertSame(90.0, $this->account->getBalance());

    // withdraw rest of money
    $this->account->withdrawMoney(90.0);
    $this->assertSame(0.0, $this->account->getBalance());
}

/**
 * @expectedException InvalidArgumentException
 */
public function testWithdrawMoneyWithoutSufficientBalance()
{
    // make sure that initial balance is zero
    $this->assertSame(0.0, $this->account->getBalance());

    // try to withdraw 100
    $this->account->withdrawMoney(100);
}

Tím bychom měli otestovány metody pro vložení i výběr částky z účtu. Zbývá nám testem ověřit poslední část zadání – je-li správně udržován seznam změn na účtu. Seznam změn je implementován pomocí pole – po každém korektním výběru je do seznamu přidán text: „withdraw: [amount]“ a po každém korektním vkladu: „deposit: [amount]“. Zkusme tedy provést několik operací a ověřit, zda seznam změn odpovídá našemu předpokladu.

public function testGetChanges()
{
    // check initial state
    $this->assertSame(array(), $this->account->getChanges());

    $this->account->depositMoney(100);
    $this->assertSame(1, count($this->account->getChanges()));
    $this->assertContains(
        "deposit: 100.00",
        $this->account->getChanges());

    $this->account->withdrawMoney(99.99);
    $this->assertSame(2, count($this->account->getChanges()));
    $this->assertContains(
        "withdraw: 99.99",
        $this->account->getChanges());

    $this->assertSame(
        array("deposit: 100.00", "withdraw: 99.99"),
        $this->account->getChanges());
}

Ani zde nesmíme zapomenout na možnosti nekorektních vstupů. Tentokrát budeme případné výjimky ignorovat, zajímá nás pouze obsah seznamu (pole).

public function testGetChangesAfterBadDeposit()
{
    $this->assertSame(array(), $this->account->getChanges());
    try {
        $this->account->depositMoney(-1);
    } catch(InvalidArgumentException $e) {/* ignore exception */}

    $this->assertSame(array(), $this->account->getChanges());
}

public function testGetChangesAfterBadWithdraw()
{
    $this->assertSame(array(), $this->account->getChanges());
    try {
        $this->account->withdrawMoney(100);
    } catch(InvalidArgumentException $e) {/* ignore exception */}

    $this->assertSame(array(), $this->account->getChanges());
}

Máme hotovo, spustíme PHPUnit.

$ phpunit .
PHPUnit 3.6.11 by Sebastian Bergmann.

........

Time: 0 seconds, Memory: 3.25Mb

OK (8 tests, 20 assertions)

Změny, změny a další změny

Už už se chystáme odevzdat svou práci, když zjistíme, že management banky přišel s novým požadavkem: jeden účet musí být schopen převést částku na jiný účet. Aby toho nebylo málo, architekt systému požaduje, aby toto bylo implementováno metodou, jejímž prvním parametrem bude instance účtu příjemce a druhým parametrem bude převáděná částka. Do seznamu změn na dotčených účtech mají přibýt texty: „sent [amount]“ u účtu odesílatele a „received [amount]“ u účtu příjemce. Nikdo bohužel nezaručí, že nemůže dojít k případu, kdy je příjemcem částky jiná instance účtu stejného majitele a jeden majitel by tedy převáděl částku sám sobě. Což je samozřejmě nesmysl a musíme na to pamatovat. Zadání vyžaduje, abychom před převodem ověřili, zda celé jméno příjemce není stejné jako celé jméno odesílatele. Pokud se jména rovnají, pak je vyžadována výjimka InvalidArgumentException. Stejně jako ve všech předchozích případech platí ochrany proti nekorektním vstupům.

Ač se na první pohled jedná o složité zadání, implementovat tento požadavek nebude těžké. Pojďme si ukázat, jak postupovat podle metodiky Test driven development. Nejprve si na nově požadovanou funčnost napišme testy a postupujme podle známých faktů. Na účet odesílatele vložíme prvotní vklad 200 a odešleme částku 99.99 na účet příjemce, jehož počáteční zůstatek je nula a nemá žádné záznamy o změnách. Zůstatky obou účtů po převodu by měly být: 100.01 (odesílatel) a 99.99 (příjemce). Oba účty by také měly mít záznamy o provedené transakci.

public function testTransfer()
{
    $recipient = new BankAccount("Jane", "Dawn");

    $this->account->depositMoney(200);
    $this->account->transfer($recipient, 99.99);

    $this->assertSame(100.01, $this->account->getBalance());
    $this->assertSame(
        array("deposit: 200.00", "sent: 99.99"),
        $this->account->getChanges());

    $this->assertSame(99.99, $recipient->getBalance());
    $this->assertSame(
        array("received: 99.99"),
        $recipient->getChanges());
}

Tím máme připravený test pro případ korektního převodu. Přistoupíme k testu pro případ převodu na účet stejného majitele:

public function testTransferToSameAccount()
{
    $recipient = new BankAccount("John", "Doe");

    $this->account->depositMoney(200);

    try {
        $this->account->transfer($recipient, 99.99);
        $this->fail("Cannot transfer money to the same account");
    } catch(InvalidArgumentException $e) {/* ignore exception */}

    $this->assertSame(200.00, $this->account->getBalance());
    $this->assertSame(
        array("deposit: 200.00"),
        $this->account->getChanges());

    $this->assertSame(0.0, $recipient->getBalance());
    $this->assertSame(array(), $recipient->getChanges());
}

Opět vložíme nějakou počáteční částku a pokusíme se něco převést na účet stejného majitele. Podle zadání by mělo v takovém případě dojít k vyvolání výjimky. My ale potřebujeme kromě tohoto faktu ještě otestovat, zda odpovídají zůstatky a seznamy změn. Tedy zda nedošlo před vyvoláním výjimky k nějakým změnám na dotčených účtech. Použijeme jednoduchý trik: volání metody transfer umístíme do bloku try/catch a ihned za ní zavoláme metodu frameworku: fail, která způsobí selhání testu. Pokud je v metodě transfer vyvolána víjimka, pak je volání fail přeskočeno do bloku catch. V opačném případě test správně selže. Stejně tak, je-li výjimka jiné třídy než InvalidArgumentException. Nakonec ověříme, zda zůstatky a seznamy změn odpovídají předpokladu.

Nyní už zbývá jen napsat testy pro případy nekorektních převodů – převod záporné nebo nulové částky, převod částky vyšší než zůstatek. Pro odchycení a test výjimky použijeme stejný trik jako v předchozím případě.

public function testTransferBadAmount()
{
    $recipient = new BankAccount("Jane", "Dawn");

    try {
        $this->account->transfer($recipient, -100);
    } catch(InvalidArgumentException $e) {/* ignore exception */}

    $this->assertSame(0.0, $this->account->getBalance());
    $this->assertSame(array(), $this->account->getChanges());
    $this->assertSame(0.0, $recipient->getBalance());
    $this->assertSame(array(), $recipient->getChanges());
}

public function testTransferWithInsufficientBalance()
{
    $this->account->depositMoney(100);
    $recipient = new BankAccount("Jane", "Dawn");

    try {
        $this->account->transfer($recipient, 100.01);
    } catch(InvalidArgumentException $e) {/* ignore exception */}

    $this->assertSame(100.00, $this->account->getBalance());
    $this->assertSame(
        array("deposit: 100.00"),
        $this->account->getChanges());

    $this->assertSame(0.0, $recipient->getBalance());
    $this->assertSame(array(), $recipient->getChanges());
}

Tím jsme připraveni na vlastní implementaci požadované změny. Pokud nyní pustíme testy, pak nás asi moc nepřekvapí, že dojde k chybě:

$ phpunit .
PHPUnit 3.6.11 by Sebastian Bergmann.

........PHP Fatal error:  Call to undefined method BankAccount::transfer()

Metoda transfer samořejmě ještě neexistuje a díky tomu není možné testy spustit. Implementujeme tedy minimální možný kód, abychom testy zprovoznili. Zbytek zatím neřešíme, jde nám pouze o zprovoznění testů.

/**
 * @param BankAccount $recipient
 * @param float $amount
 * @return float
 * @throws InvalidArgumentException
 */
public function transfer(BankAccount $recipient, $amount) {}

Nyní už testy spustit jdou, ale v kódu třídy BankAccount máme chyby. Což je logické, metoda není implementována.

$ phpunit .
PHPUnit 3.6.11 by Sebastian Bergmann.

........FF..

Time: 0 seconds, Memory: 3.50Mb

There were 2 failures:

1) BankAccountTest::testTransfer
Failed asserting that 200.0 is identical to 100.01.

~/test/BankAccountTest.php:118

2) BankAccountTest::testTransferToSameAccount
Cannot transfer money to the same account

~/test/BankAccountTest.php:135

FAILURES!
Tests: 12, Assertions: 29, Failures: 2.

V prvním případě nesouhlasí zůstatky, to budeme řešit jako první. Opět napíšeme minimální kód ke splnění testu – z účtu odesílatele odečteme částku a příjemci ji připočítáme.

public function transfer(BankAccount $recipient, $amount)
{
    $amount = (double)sprintf("%.02f", (double)$amount);
    $this->subAmount($amount, "sent: " . sprintf("%.02f", $amount));

    $recipient->depositMoney($amount);
}

Chybu jsme opravili, ale stále to není vše. Jako v předchozím případě – vezmeme první z nich:

$ phpunit .
PHPUnit 3.6.11 by Sebastian Bergmann.

........FFFF

Time: 0 seconds, Memory: 3.50Mb

There were 4 failures:

1) BankAccountTest::testTransfer
Failed asserting that Array (
    0 => 'deposit: 99.99'
) is identical to Array (
    0 => 'received: 99.99'
).

Tím, že jsme pro připsání částky na účet příjemce použili metodu depositMoney, jsme způsobili, že se sice částka přičetla, ale zpráva v seznamu změn neodpovídá požadavku. Ke splnění tohoto testu máme minimálně dvě možnosti: buď přidáme metodu pro příjem částek „jinak než vkladem“, a nebo prostě přidáme metodě depositMoney druhý parametr, nesoucí řetězec s „důvodem“ příjmu částky. Využijeme druhou možnost. Splníme tím požadavek na minimální kód, který projde testy a pokud přijde v budoucnu další podobný požadavek, kód refaktorujeme. Tato změna způsobí selhání dřívějších testů, proto připojíme jeho defaultní hodnotu: „deposit“:

public function depositMoney($amount, $reason = "deposit")
{
    $amount = (double)sprintf("%.02f", (double)$amount);
    if ($amount <= 0) {
        throw new InvalidArgumentException(
            "Cannot deposit negative amount of money");
    }

    return $this->addAmount($amount,
        $reason . ": " . sprintf("%.02f", $amount));
}

public function transfer(BankAccount $recipient, $amount)
{
    $amount = (double)sprintf("%.02f", (double)$amount);
    $this->subAmount($amount, "sent: " . sprintf("%.02f", $amount));

    $recipient->depositMoney($amount, "received");
}

Spustíme testy, seznam změn je ok, další chybou je:

1) BankAccountTest::testTransferToSameAccount
Cannot transfer money to the same account

Napíšeme minimální kód pro splnění testu – ověření, zda nepřevádíme částku na ten samý účet.

public function transfer(BankAccount $recipient, $amount)
{
    if ($this->getOwnerName() === $recipient->getOwnerName()) {
        throw new InvalidArgumentException(
            "Cannot transfer money to the same account");
    }

    $amount = (double)sprintf("%.02f", (double)$amount);
    $this->subAmount($amount, "sent: " . sprintf("%.02f", $amount));

    $recipient->depositMoney($amount, "received");
}

Po spuštění testů zbývají dvě chyby. Jedna se týká neplatného vstupu a druhá nedostatečného zůstatku. Vezmeme obě najednou a napíšeme minimum pro splnění – ověření vstupu, zda je kladný a ověření, zda převáděná částka není větší než zůstatek na účtu.

public function transfer(BankAccount $recipient, $amount)
{
    if ($this->getOwnerName() === $recipient->getOwnerName()) {
        throw new InvalidArgumentException(
            "Cannot transfer money to the same account");
    }

    if ($amount <= 0) {
        throw new InvalidArgumentException(
            "Cannot deposit negative amount of money");
    }

    if ($this->getBalance() < $amount) {
        throw new InvalidArgumentException("Insufficient balance");
    }

    $amount = (double)sprintf("%.02f", (double)$amount);
    $this->subAmount($amount, "sent: " . sprintf("%.02f", $amount));

    $recipient->depositMoney($amount, "received");
}

Že by nyní?

$ phpunit .
PHPUnit 3.6.11 by Sebastian Bergmann.

............

Time: 0 seconds, Memory: 3.50Mb

OK (12 tests, 36 assertions)

Ano! Všechny naše předpoklady byly splněny. Pokud jsme do testů zanesli všechny kritické body zadání, můžeme refactoring prohlásit za hotový. V opačném případě pokračujeme stejným způsobem – napíšeme testy, přidáme nejkratší možný kód ke splnění testů, a je-li třeba, refaktorujeme.

Jestli vás tento dnešní příklad zaujal, tak si můžete zkusit pokračování – management banky se rozhodl otevřít pobočku v České republice a protože zjistil, že na tamnějším trhu velice dobře fungují poplatky, tak zadal změnový požadavek: každá transakce bude zpoplatněna. Poplatky musí být zaneseny do seznamu pohybů na účtu a ke stržení poplatku může dojít i v případě, že klient nemá dostatačný zůstatek.

To je pro dnešek vše, v dalším díle se pustíme do těch, už dvakrát odložených, anotací. Příklady budou spíše „inline“, na další větší praktický příklad se budete moci těšit přes-příště. Všechny uvedené zdrojové kódy jsou dostupné na mém Githubu:

https://github.com/josefzamrzla/serial-testovani-v-php

Komentáře

Subscribe
Upozornit na
guest
48 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
michal

Díky za článek, ale pořád je pro mne tento příklad na úrovni „příkladu s kalkulačkou“, tedy lehce otestovatelné, predikovatelné. V reálných aplikacích vidím daleko složitější a více problémové části kódu k otestování.

arron

V tomto článku možná chybí nějaká externí závislost, což je při rozsahu příkladu trochu škoda. To by Vám ukázalo, jak se s takovou věcí v testech vypořádat.

Jinak v reálných aplikacích by správně měly být testy úplně stejně jednoduché, protože každá metoda, kterou testujeme, dělá jednu jedinou jednoduchou věc :-) V praxi je to zpravidla o dost složitější, ale při hlubším zamyšlení vždy docházím k tomu, že na vině je špatně navržený a napsaný kód.

A ano, kód, který nerespektuje základní požadavky, jako třeba Single Responsibility Principle, DI pattern a podobné, tak takový kód se opravdu pekelně blbě testuje!

Martin Hassman

Však napřed to jednoduché, až pak teprve složité. Seriál se stále rozjíždí, není ani v polovině, není nutné nacpat hned vše do jednoho dílu.

michal

Jsem zvědav jak to celé dopadne. Zásadní na testování vidím pouze toto: „Jak napsat/navrhnout otestovatelnou aplikaci“. Vše ostatní mi připadá oproti tomu „jednoduché“

Aleš Roubíček

Stačí psát nejprve testy, pak máte testovatelnou aplikaci jako výsledek vždy.

michal

Ok, těšíme se. Jen ne prosím opět opsaný příklad z tutoriálu PHPUnit

Martin Hassman

To je podobné, jako kdybyste obvinil autory všech knížek, co použili „Hello world“, že je opsali. Neopsali, některé věci se prostě opakují znovu a znovu, ať Hello world nebo příklad s bankovním účtem.

Diskobolos

Přesně tak! Je to o jiném způsobu uvažování a zpracovávání problémů.

lenochware

Mě se zdá, že skutečná výhoda unittestů se projeví až ve chvíli, když pracuji v aplikaci ve větším týmu programátorů nebo vyvíjím nějaké reusabilní komponenty používané více lidmi. Tam je to pak opodstatněné, protože když někdo jiný udělá úpravu, tak může spustit testy a ověřit jestli něco nerozbil.

Výhoda, že spustím testy a vidím jestli se něco nerozbilo platí sice i u jednoho programátora, který píše dejme tomu jednoduchý shop, ale za všechno se musí platit a tady platím imho o dost delší dobou vývoje danou psaním testů. Navíc mám za to, že unittesty nenahrazují betatesting – imho můžu víceméně otestovat jen chyby o kterých vím, že můžou nastat. Takže u menšího projektu bych asi zvažoval jestli se to vyplatí…

Čelo

Já myslím, že se to nevyplatí jen v případě, že je vaším záměrem to rychle nabastlit s tím, že doufáte, že už to nikdy v životě neuvidíte ;)

arron

„…platím imho o dost delší dobou vývoje danou psaním testů…“

A to je právě ten nejzásadnější nesmyslný předsudek :-) Samozřemě, že chvíli trvá, než se člověk naučí unit testy psát. Úplně stejně, jako nějakou chvíli trvá se naučit nějaké API. Nicméně ve finále člověk, který nemá unit testy stráví tolik času laděním, hledáním chyb, opravováním chyb při změně kódu apod. tolik času, že psaní unit testůz toho vychází o dost výhodněji :-)

Budela

A to je právě předsudek. Tohle tvrzení jsem slyšel snad tolikrát, ale pořád se to předkládá jen jako nějaký axiom. Jako něco co není potřeba dokazovat. A já tomu stejně nevěřím. Chtěl bych aby někdo dokázal, že když budu psát testy, tak nad nimi strávím x hodin, ale když je nenapíšu, tak strávím Y hodin laděním a hledáním kdovíčeho. Já tomu prostě nevěřím.

arron

Není to axiom, je to kalkul. Analogie s dřevorubcem není uplně přesná, ale myslím, že jí tady pro demostraci mohu použít :-)

Dřevorubec kácí v lese stromy a musí jich každý den pokácet nějaký počet. Postupně se mu ztupí sekera a on je stále pomalejší a pomalejší má velký problém tu svojí kvótu splnit. Když se ho pak někdo zeptá, proč si sekeru nenabrousí, tak dřevorubec odpoví, že nemá čas, protože jinak to nestihne.

Psaní testů je takové broušení sekery.

Pokud se vrátím k tomu kalkulu. Co je rychlejší? Dělat pořád dokola činnost, která mi zabere hodinu (ruční testování sw, hledání chyb apod.) nebo strávit deset hodin automatizací této činnosti a pak jí vykonávat v řádu vteřin po dobu zbytku vývoje projektu?

Protože se ve vývoji webových aplikací už nějkou dobu pohybuji, tak na tyto počty opravdu nepotřebuji vědeckou studii ;-)

Budela

Tak já nevím, mně přijde, že když budu psát kvalitní objektový návrh, testovatelný kód (bez testů) tak si tu sekeru nabrousím lépe.

Navíc si nemyslím, že rozdíl mezi psaním testů a nepsaním testů je v řádu vteřin. Párkrát jsem to zkoušel a třeba takové TDD je podle mého opruz a ztráta času dost velká. Pokud se několikrát během psaní aplikace rozhodnu změnit rozhraní třídy, tak to znamená pořád dokola měnit spoustu testů. Navíc každá změna specifikace znamená rozbití testů a jejich následná oprava. Rozhodně to není v řádech vteřin.

Netvrdím, že testování je špatné a že bez něho je vývoj lepší. Chci pouze říct, že tvrzení o tom, že psaním testů člověk ušetří čas, se mi zdá jako přehnané. Tvrdí to všichni, tvrdí to často a podle mého se mýlí. Podle mého je psaní testů časově náročnější, ale jsou z toho jiné benefity.

arron

Já také netvrdím, že psaním testů člověk ušetří čas přímo při psaní kódu. Já tvrdím, že ušetřím čas při testování, ladění a hledání chyb. Zmiňovaný časový rozdíl je zde mezi ručním testováním (čili F5 hell) a tím, že pustím sadu testů, které totéž udělají v řádově kratším čase :-) Testy mohu pouštět třeba 100x denně, zatím co ruční testování celé aplikace po každém commitu…ani to nechci domýšlet :-) Vidítě „jistý“ časový rozdíl?? A kvalitativní? Jaká je pravděpodobnost, že rychle odhalím chybu, když testují 100x denně a jaká, když „to večer projedu ručně“?

Každá změna specifikace hlavně znamená rozbití aplikace ;-) Testy mi pomohou zjistit, kde všude se aplikace rozbila a pomohou mi jí opravit. Následně mi řeknou, jestli je oprava kompletní a funkční.

TDD imho dost selhává ve chvíli, kdy nevím co píšu,čili nevím, co mám testovat (ačkoliv jedna z přednášek na Webexpu mě nutí tento názor začít přehodnocovat). Osobně si však myslím, že v tomto směru selkávám spíše já než TDD. Je to prostě opravdu změna mindsetu.

anonym

Argument proti nezpochybňoval výhodu automatického testování. Zdůrazňoval nevýhodu přepisování již hotových testů v momentě, kdy se mění rozhraní.

Tento článek zatím jen ukázal, jak se testuje. Fajn, ale příklad je příliš vyumělkovaný, podobného kódu mám v běžné webové aplikaci tak 2-10 metod, u kterých potenciál testování nezpochybňuji. Ale TDD by mě měl tlačit testovat vše a to si neumím prakticky představit. Doufám,že tento seriál neumře dříve, než v něm bude napsána kompletní reálná funkční aplikace (i kdyby blbý blog nebo pidieshop) a na ní předvedeno TDD v praxi.

Clary

Nejde pouze o testování samotné. Nedávno jsem si dělal malou aplikaci a vykašlal jsem se na testy s tím, že je doplním později. Když jsem později začal testy doplňovat, zjistil jsem, že jsem nevědomky na mnoha dílčích místech napsal netestovatelný=špat­ný kód, i když jsem si celou dobu namlouval jak je aplikace krásně objektově strukturovaná. Teď musím provádět refaktoring, místo abych přidával další featury.

j

Je to celkem jednoduche – pokud mate testy, neco zmenite, tak zcela trivialne zjistite, zda to nerozbilo neco, co predtim fungovalo (ohromna uspora casu).

V opacnem pripade muze autor leda tak doufat a modlit se, protoze asi nepripada moc v uvahu nejake rozsahlejsi testovani cele aplikace kvuli „drobne“ uprave.

Ano, jsou dodavatele, kteri to resi tak, ze provedou upravu, a cekaji, az se ozvou uzivatele … (vytvory jednoho takoveho mam tu „cest“ adminovat …). Pokud nevadi nasrani uzivatele a potencial zalob o nahradu skod, je to celkem jedno.

V kazdem pripade oni stravi odladovanim i nekolik tydnu, protoze vzdycky „nekde neco opravi“, ale malo kdy vsechno co rozbili, pripadne tou „opravou“ rozbijou neco jineho.

Michal

Programovat bez testu je jako hrat hru bez savovani. O vsem co otestujes muzes rict, ze to mas pod kontrolou (jako ulozenou pozici). Aplikaci ti muze rozbit prakticky cokoli vcetne upgradu php.
Az praxe te nauci kolik trnu s tim muzes ze svych pat vytahnout nez se na ne postavis… (nez upravy nasadis)

RDPanek

Přátelé, už jen v podstatě samotného testování se skrývá důvod, proč testovat. Ať již využijete z mnoha nabízených postupů a typů testů, tak zajistíte výsledné aplikaci jistou úroveň kvality a povíte o sobě, že odevzdáváte něco co je pokryté do určité míry testy. Klientovi při předávání aplikace spustíte jednotlivé sady podle předem napsaných testplánů, klientovi v rámci přejímání řeknete, že zadaná část se zdá opravdu být v pořádku. Takže pro klioše +1. Důvod sami pro sebe, proč testovat je ten, že vaši kolegové vkročí na zpevněnou půdu a vy máte jistou úroveň jistoty, že se vám přes rozhranní nedostane nic mimo hraniční hodnoty s čím nepočítáte. Bez testování se neobejdete v CI (platí to tak všude, že?). Takže pro vás +1. Důvod pro vašeho zaměstnavatele je ten, že pokud při vývoji narazíte na jednu z několika typů chyb, tak je její oprava rychlejší/levnější, než když chybu objeví kdokoli jiný směrem k zákazníkovi.

Nic méně, ze skušenosti mohu říct, pokud vám testy nedávají smysl, tak ani netestujte – dají se napsat testy, které zdánlivě vypadají v pořádku, vykazují určité procento pokrytí, ale ve skutečnosti jsou napsány špatně.

Ideálně, kdyby unit testy psal sám programátor a ostatní typy testů jiný programátor / tester / QA.

Obecně platí, že se nevyplatí vyvíjet aplikaci bez toho, aniž by se počítalo s pokrytí testy.

none_

Základní problém je, že někdo musí managerovi a zákazníkovi vysvětlit, že díky tomu, že strávíme stejně času psaním unit testů jako programováním, on ve výsledku ušetří.

To se bohužel dost těžko vysvětluje, pokud to první nabídku zdraží o X Kč…

arron

Ufff…

Vždyť tohle přece není pravda.

Projekt má rozpočet 100%. Obvykle je to tak, že se z toho rozpočtu řekněme (příklad) 50% programuje, 20% testuje a 30% se opravují chyby.
Pokud budu testovat, tak budu 70% programovat, testovat budu průběžně pomocí CI a automaticky, takže ten čas tam započítávat nemusím, 10% času budu opravovat bugy, které jsem nepokryl testy (a pokryju je testy) a zbývá mi 20% rozpočtu na to abych se třeba věnoval klientovi. Kde je jaké navýšení??
Ty poměry jsou pro příklad, protože jsou hodně individuální…

Problém spíše vidím v tom, že se netestuje vůbec a v rozpočtech se s tím obvykle vůbec nepočítá (a nebo jsou odhady VELMI optimistické). Zpravidla to pak testuje klient za provozu. Je to jeden z hlavních důvodů, proč se téměř každý projekt protáhne a prodraží a ještě k tomu moc nefunguje…

RDPanek

ano, to jsou zpravne argumentace. Nic mene toto tema je tak casto promilano stejne tak jako jestli je lepsi windows nebo linux a stejne si jede kazdy po svym podle sveho presvedceni.

Toto tema neni vhodne pro diskuzi tohoto clanku, pokud nemoku nedava smysl testovat, doporucuji navstivit skoleni, kde se ucastnici dozvedi, druhy chyb, jejich vznik a jejich dopad.

Honza

Vidím že se rohořela diskuze stejně plamenná jako u předchozího článku, tak přihodím jedno kontroverzní polínko, aby to ještě pokračovalo:

Programátoři jsou různí. Někdo je chytřejší někdo hloupější, někomu to lépe myslí v jednom směru, jinému v jiném. Někdo dokáže vymyslet abstraktní konstrukce, někdo dokáže rovnou přemýšlet v SQL, někdo je zase extrémně pečlivý a puntičkářský.
Pokud o sobě vím, že patřím k lidem, kteří dokážou jít za celkovým cílem a v rychlosti vychrlit kód pro několik rutin za sebou, tak musím mít napsané automatické testy, protože se bez nich neobejdu. Pokud o sobě vím, už při psaní kódu dokážu pečlivě promýšlet všechny cesty, kterými může běh projít a celou rutinu při psaní mentálně obsáhnu, tak automatizované testy potřebuju podstatně méně.

Všimněte si, že se záměrně vyhýbám tomu, označovat někoho za lepšího a někoho za horšího programátora. Prostě jen někdo testy nepotřebuje, protože testy stejně otestuje jen to, co ho otestovat napadne a to ví už při psaní, že je správně.

arron

Podle „druhu programátora“ bych spíše zvolil úpravu metodiky. Já osobně napřiklad někdy chytnu slinu, nějak to zbastlím a pak strávím nějaký čas psaním testů a refaktoringem. Jindy zase opravdu napíšu nejdřív testy a pak teprve implementuji…jak to zrovna přijde.

Tak jak jste to napsal to skoro vypadá, že někteří programátoři nedělají chyby :-) Můžu Vás ujistit, že to není pravda ;-)

Honza

Nevěřím, že existuje programátor, který nedělá chyby. Ale je jasné, že různí programátoři dělají různé chyby. A různé chyby se různě hledají. Domnívám se, že někteří programátoři můžou mít díky charakteru chyb, které dělají, větší užitek z automatizovaného testování než jiní.
V překladu – jsou i programátoři, kteří nedělají tolik chyb, které jsou odhalitelné automatickými testy, aby se jim vyplatilo automatické testy psát.

j

Pokud ty testy pojmete truchu do sirky, muzete vygenerovat i zatezovy test aplikace, a tady se uz neda mluvit o chybach programatora, protoze ta aplikace „nejak“ funguje, ale casto zni otazka „jak“.

Dost si nedovedu predstavit, jak neco takoveho dela nekdo „rucne“, pripadne jak analizuje kod a zjistuje, kde jsou vykonostni problemy.

Honza

Zátěžové testy jsou něco úplně jiného a dělají se úplně jinak.

Jiří Knesl

Ale všichni pracujeme v prostředí, kdy nás někdo vyrušuje. Kdy musíme přepínat kontexty. Kdy nejsou jen pondělky, úterky a středy, ale kdy programujeme i v pátek odpoledne a nemyslí nám to tolik, jako v pondělí. Jsou dny, kdy se doma něco stane a v práci se nemůžeme soustředit. Používáme nové technologie a postupy, frameworky, které neznáme.

A tohle je realita nás všech, výsledkem čehož je, že se dostáváme do situací, kdy každý udělá nějaké chyby, ikdyby byl sebevíc svědomitý. A testy jsou zatím lidstvu nejznámější prostředek detekce chyb.

Honza

Klasický červený sleď.
Nevím, jestli jste si toho všiml, ale nikde jsem nepsal, že je někdo, kdo nedělá chyby. Tzn. vybral jste si k rozporování něco, co jsem vůbec nenapsal a ani si to nemyslím.

Čili ještě jednou, jen zjednodušeně – různí programátoři dělají různé chyby, různé chyby se různě hledají. Automatizované testy jsou jedním ze způsobů hledání chyb a pro některé programátory tudíž prostě nemusí být efektivní je používat, protože neodhalí chyby, které dělají, ale jen ty, které nedělají.

Jiří Knesl

Reaguju: „Pokud o sobě vím, už při psaní kódu dokážu pečlivě promýšlet všechny cesty, kterými může běh projít a celou rutinu při psaní mentálně obsáhnu, tak automatizované testy potřebuju podstatně méně.“

Při běžném provozu (neustálé vyrušování, méně energie v pátek odpoledne, menší soustředění apod.) tvrdím, že prostě není možné chyby nedělat a proto není možné se bez nějakého prostředku obejít (a testy považuju za nejznámější, byť ne jediný prostředek). Věřím, že myšlenka, že dokážete promyslet všechny cesty, kterými může běh projít, je u netriviálních aplikací nereálná.

Honza

Asi nemá smysl se dohadovat dál. Odmítáte přistoupit na to, že je možné dělat různé druhy chyb. Nikde jsem neříkal, že by měl být kdokoliv imunni vůči jakýmkoliv chybám. Pouze si myslím, že chyby jsou různé – od špatného návrhu (který vám automatizované testy rozhodně neodhalí) až k překlepům (které odhalí už syntaktická analýza) – a že ne pro všechny druhy chyb se automatizované testování hodí.

Honza

To je myslím nedorozumění – rozdíl si dobře uvědomuji. Testování návrhu a chyby syntaxe jsem myslel právě jako příklady, kdy nemá o automatizovaných testem smysl hovořit.

Honza

Nevím, jestli jste četl celé vlákno, ale já jsem obhajoval myšlenku, že jsou různé druhy chyb a na některé z nich je možné automatizované testy používat a na některé z nich ne. Což je přesně to, co píšete o příspěvek dřív, takže vcelku nevidím rozpor.

mino

Tvrdite, ze existuju chyby, ktorych sa urciti ludia nedopustaju. Oponenti sa Vam snazia povedat, ze toto tvrdenie je naivne a nebezpecne. Pripustme povedzme, ze niektori ludia spravia priemerne menej chyb, (ktore mozu rozbit aplikaciu na uplne opacnom konci.., resp. doplnte si typ chyby, ktory mate na mysli) ako ini ludia. Stale vsak je naivne tvrdit, ze tito ludia nespravia ziadnu takuto chybu. Aj ked je takato chyba v release jedna, a zisti sa povedzme po tyzdni ostrej prevadzky, je to zbytocne, ked sa jej dalo predist jednym spustenim testu na dev prostredi a jednym na produkcii po release. Ked k tomu pridam este hodnotu financej straty klienta, ktoru tymto sposobom samozrejme nemozme charakterizovat (i. e. niektori programatori proste robia chyby, ktore su lacne, niektori, ktore su drahe, ti, ktori robia iba lacne chyby, unit testy nepotrebuju – dufam, ze s tymto polemizovat nebudete) tak by ste snad mohol zacat vidiet prinos testov.

Skor by som sa priklonil k myslienke, ze niektore aplikacie nepotrebuju testovanie, resp. ho potrebuju menej ako ine aplikacie. Napada ma napr. ‚Hello world‘ : )

Aby som sa nevyjadroval absolutne, tak kazda aplikacia, ktora sa sklada z viac ako jednej „jednotky“ (unit) moze z pritomnosti unit testu iba ziskat.

Jiří Knesl

Asi fakt mluvíme o voze a o koze.

Já vůbec neodmítám, že jsou různé druhy chyb. Jsou chyby, na které testy nepřijdou.

Já odmítám myšlenku, že při jakémkoliv psaní kódu je možné psát tak, aby současně s jinými chybami nevznikaly i chyby, které testy odhalí.

Honza

V tom případě se zřejmě shodujeme, protože nemám pocit, že bych někde tvrdil, že je možné psát kód tak, aby určitý druh chyb vůbec nevznikal – pouze to, že někdo má větši tendenci k jednomu druhu chyb a někdo k jinému.
Pokud o sobě vím, že dělám opravdu málo chyb, které mohou odhalit automatizované testy, ale za to často úplně zvořu návrh, je pak pro mne užitečné věnovat více času přemýšlení nad návrhem a méně času (0?) věnovat psaní automatizovaných testů.
Je to takhle pro vás ještě akceptovatelné?

https://vladimirpilny.mojeid.cz/#2oFvdzWbch

Líbí se mi myšlenka TDD, že by se dalo testovat automaticky a rychle odhalit narušení již funkčního kódu. Problém je, že všechny „školní ukázky“ jako je tento, jsou v podstatě kvůli nenáročnosti zbytečné (testování sčítání 2 čísel).
Projekty v PHP, se kterými pracuji, mají mnoho různých úrovní a je potřeba testovat velké množství různých situací na straně serveru i klienta a na to už žádné názorné ukázky nikde nevidím (a tak se obávám, že to ani nepůjde). Jde mi o komplexní chování – práce s databázemi, sešny a cookie, kompatibilitu HTML zobrazení a chování, běh javascriptu, funkčnost DOM, AJAXová volání, paralelní přístupy a zamykání, simulace síťových výpadků, atp. Přitom na pozadí běží různí démoni a údržboví čističi, takže i DB se průběžně mění…
Navíc ve chvíli, kdy vstupem není číslo či řetězec, ale např. ZIP archív posílaný po síti, který mám rozbalit, a něco s obsahem udělat, tak je asi milion věcí, co se může pokazit vzhledem k problematickému vstupu.
Pokud někdo víte, jak tohle všechno automaticky testovat, tak sem s tím. Jinak mi vychází, že nejlepší je, když vývoj vede velmi zkušený vývojář, který zná dobře všechna zákoutí všech použitých technologií a hlavně u toho přemýšlí hlavou (a ne jen poučkami z učebnic softwarového inženýrství). :)

jachacha :D ty seš dobrý jelítko

Michal

Jsi na ne moc hrrr. Vydrz casu, serial ti postupne odpovi.

1) unit testing: V tomhle dile jde o testovani tridy ktera nema externi zavislosti. To znamena, ze testujes prime hodnoty in/out – to je prece super priklad na zacatek no ne?
Za par dilu pokroci serial 100% k testovani zavislosti na zaklade „fejkovani“ instanci cizich trid, ktere testovana trida vyuziva. Proto abys takove tridy mohl testovat, musis dodrzovat urcita pravidla. Napriklad nikde v tride nesmis mit zavislosti hardcodovane. Pikud jsi nekde kolem zachytil pojem „Dependency injection“ tak ten se tyka prave tohoto. Pomoci DI udrzujes tridu vysoce testovatelnou, protoze ti to v ramci unit testu umoznuje nahradit tyto zavislosti takzvanymi „mocky“ – fejkovymi instancemi, ktere dovedou naslouchat tomu co s nimi tva testovana trida dela a na zaklade toho ty se potom muzes v tech testech muzes ptat jestli se testovana trida opravdu snazila „ziskat uzivatelovo jmeno z databaze“ a taky treba kolikrat se o to snazila.
V ramci unit testu ti toto musi stacit protoze testujes konkretni ocekavane chovani jedne tridy – jednotky kodu.
Nemusim asi zduraznovat, ze toto tve ocekavani chovani tridy, ktere ty temi testy pokryjes se muze zborit jakymkoli vlivem vcetne upgradu PHP na novejsi verzi. To je obrovskej prinos unit testu!

2) integracni testing – dalsi stupen testovani, ktereho se tyka tva poznamka – v tzv. integracnich testech jde o to, jak tve jednotky komunikuji mezi sebou a dale treba i s databazi. Tim se tedy zacinas priblizovat konkretnim situacim, ktere ti muzou nastat v realnych podminkach v aplikaci.
V integracnim testu si napriklad nastavis konkretni prostredi (napriklad nahazis do kosiku eshopu nejake produkty) a testujes jestli ti jakasi trida kosiku (napriklad) vraci spravne cenu celkem, cenu s dani apod…
Videl jsem hodne aplikaci, ktere mely pouze integracni testy a zadne unit testy ani neobsahovaly. To ale neni idealni, protoze unit testy jsou nejblize potencialni pricine problemu a vetsinou ti umozni hcybu odhalit doslova primo v miste kde vznikla.

3) testy chovani – dalsi stupen testovani aplikace kde uz testujes realne http komunikaci, obsah DOM documentu a konkretnich css selectoru, http hlavicky apod. – temi uz jsi schopen overit co presne aplikace dela vuci browseru a uzivateli – da se rict ze testy chovani aplikace za tebe aplikaci proklikaji.
Nektere frameworky je samy obsahuji a casto ti jejich psani zjednodusuji na absolutni minimum – jako treba tady http://symfony.com/doc/current/book/testing.html#your-first-functional-test .

– Existuji projekty, ktere ti umoznuji velmi snadno automatizovane testovat kazdy tvuj commit (pokud samozrejme pouzivas nejaky SCM jako je treba git).
Takovym je treba https://travis-ci.org/ – vlevo muzes videt defakto v realtimu aktualne problehle testy a postupne se proklikat na jejich resume (zelene = success, cervene = failed) a dale primo na jejich vystup z command lajny.

Jsou tam dokonce i nejakej ceske Nette buildy..

Pokud nevis co je „dependency injection“, urcite ti neublizi, kdyz si najdes nejake skoleni, ktere se DI programovani tyka. Hodne se to tyka testovani jako takoveho a jsem si jistej, ze te to hodne posune.

https://vladimirpilny.mojeid.cz/#2oFvdzWbch

Díky za podrobnou odpověď. Díval jsem se na první díl seriálu, takže je mi jasné, že jsem se ptal víc na integrační a funkční testy. Tak já tedy zkusím počkat na další díly a snad se dozvím odpovědi.
DI opravdu nerozumím, ale popravdě mě ani moc netrápí, protože povětšinou neprogramuji OOP.

a pokud se odpovědi nedozvíš, tak hlavně prosimtě nechoď do diskuzí psát jak je automatizovaný testování naprd, protože tě k tomu nikdo nepřivedl za ručičku; s takovým přístupem se asi těžko naučíš něco novýho – začíná snad nějaký povídání o programovacím jazyce jinak než s hello world?

PS: sorry za to jelítko, nemoh sem si pomoct

PPS: TDD se má k testování asi jako způsob zvednutí auta k výměně kola

https://vladimirpilny.mojeid.cz/#2oFvdzWbch

Rozdmýchávat flamy opravdu nechci, nebojte. Pravda, já se nechci učit NOVÉ věci, které se za mých studií na MFF ještě nevyučovaly, jen proto, abych ukázal, jak jsem moderní. Ale chci se učit řešit reálné problémy LÉPE. Takže doufám, že se dozvím, jak efektivněji testovat i komplexní chování.

P.S. Stane se. :)

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.