Mockovanie objektov

05/2015 / Milan "perún" Herda / @moriquend / hrad.perunhq.org

O čom budeme hovoriť

  • Druhy testov (úrovne a typy)
  • Čo je mock?
  • Ako vytvárať mocky v PHPUnit
  • Návratové hodnoty
  • Návratové hodnoty v závislosti od parametrov
  • Počítanie volaní
  • Iné knižnice (Mockery)

Druhy testov

Podľa úrovne

  • unit testy
  • integračné testy
  • component interface testing
  • systémové testy

Unit testy

Cieľom je otestovať fungovanie jednej jednotky kódu

Jednotka sa testuje izolovane od iných jednotiek kódu.

Jednotkou môže byť trieda alebo funkcia.

Integračné testy

Testovanie jednotlivých modulov ako skupiny.

Jednotky sa už netestujú izolovane, ale v spolupráci s blízkymi objektami.

Testujeme rozhrania a spoluprácu medzi modulmi.

Integračný test môže siahať do kontrolovanej databázy, alebo na kontrolované API.

Component interface testing

Rozšírené integračné testovanie, ktorého cieľom je otestovať najmä rozhrania medzi komponentami systému

Dáta prechádzajú od jedného rozhrania k druhému a medzitým sú kontrolované na platnosť.

Systémové testy

Testuje sa functionalita kompletného systému.

Testovací scenár už zahŕňa komplexnejší proces (napr. prihlásenie, následná editácia záznamu a odhlásenie).

Sledujú sa aj prípadné vedľajšie efekty.

Podľa typu

  • Installation testing
  • Compatibility testing
  • Smoke and sanity testing
  • Regression testing
  • Acceptance testing
  • Alpha testing
  • Beta testing
  • Functional vs non-functional testing
  • Destructive testing
  • Software performance testing
  • Usability testing
  • Accessibility testing
  • Security testing
  • Internationalization and localization
  • Development testing
  • A/B testing
  • Concurrent testing
  • Conformance testing or type testing
  • Čo je to mock?

    Problém

    • unit test potrebuje jednotku otestovať úplne izolovane od zvyšku systému
    • tj. nesmie sa vykonať kód nepatriaci testovanej jednotke
    
    class Article
    {
        private $isRead = false;
    
        public function markAsRead(User $user)
        {
            $this->isRead = true;
    
            $user->addToReadArticles($article); // <-- toto nepatrí testovanej jednotke
        }
    }
                        

    Ako otestovať metódu markAsRead bez toho, aby sa vykonal kód patriaci triede User?

    Použijeme dabléra (náhradníka)

    Namiesto inštancie skutočnej triedy User podsunieme volaniu markAsRead inštanciu potomka triedy User.

    Táto inštancia na volanie addToReadArticles nebude reagovať alebo bude reagovať kontrolovaným spôsobom.

    Takémuto dablérovi sa hovorí mock

    Sú dva druhy

    • stub - imituje volania metód
    • mock - od volaní metód máme očakávania (napr. presný počet volaní...)

    Nebudeme ich rozlišovať a obom budeme vravieť mock.

    Mocky potrebujeme vytvárať jednoducho a rýchlo

    
    // PHPUnit
    
    public function testMarkingAsRead()
    {
        $user = $this->getMock('User')
    
        $article = new Article();
        $article->markAsRead($user);
    
        $this->assertTrue($article->isRead());
    }
                        

    Namockovaný objekt prekryje volania všetkých metód a zabezpečí, že sa ich kód nezavolá. Defaultne budú vracať null.

    Chcem, aby volanie vrátilo nejakú hodnotu

    
    // PHPUnit
    
    public function testMarkingAsRead()
    {
        $user = $this->getMock('User')
    
        $user->method('addToReadArticles')
            ->willReturn('foo');
    
        $article = new Article();
        $article->markAsRead($user);
    
        $this->assertTrue($article->isRead());
    }
                        

    Nechcem, aby mock dedil konštruktor

    
    // PHPUnit
    
    public function testMarkingAsRead()
    {
        $user = $this->getMockBuilder('User')
            ->disableOriginalConstructor()
            ->getMock();
    
        $user->method('addToReadArticles')
            ->willReturn('foo');
    
        $article = new Article();
        $article->markAsRead($user);
    
        $this->assertTrue($article->isRead());
    }
                        

    Návratová hodnota závisí od parametra

    
    // PHPUnit
    
    public function testMarkingAsRead()
    {
        $user = $this->getMockBuilder('User')
            ->disableOriginalConstructor()
            ->getMock();
    
        $user->method('addToReadArticles')
            ->will$this->returnValueMap([
                ['param1', 'retval1'],
                ['paramN', 'retvalN'],
            ]);
    
        $article = new Article();
        $article->markAsRead($user);
    
        $this->assertTrue($article->isRead());
    }
                        

    Potrebujem overiť správnosť parametrov

    
    // PHPUnit
    
    public function testMarkingAsRead()
    {
        $article = new Article();
    
        $user = $this->getMockBuilder('User')
            ->disableOriginalConstructor()
            ->getMock();
    
        $user->method('addToReadArticles')
            ->with(
                $this->equalTo(5),
                $this->anything(),
                $this->identicalTo($article)
            )
            ->willReturn('foo');
    
        $article->markAsRead($user);
    
        $this->assertTrue($article->isRead());
    }
                        

    Metóda sa musí zavolať aspoň raz

    
    // PHPUnit
    
    public function testMarkingAsRead()
    {
        $article = new Article();
    
        $user = $this->getMockBuilder('User')
            ->disableOriginalConstructor()
            ->getMock();
    
        $user->expects($this->once())
            ->method('addToReadArticles')
            ->willReturn('foo');
    
        $article->markAsRead($user);
    
        $this->assertTrue($article->isRead());
    }
                        

    Live demo

    Alternatívne mockovacie knižnice

    Otázky?