Unit & Functional Tests w Drupal 8

Piotr Woszczyk @ 2017

Rodzaje testów natywnie wspieranych przez D8

  • Unit tests
  • Kernel tests
  • Browser & Javascript tests

Unit tests - zalety

  • Szybka implementacja (nie zawsze)
  • Szybkie wykonywanie
  • Umożliwiają łatwą identyfikacje problemów
  • Przydatne szczególnie podczas refaktoryzacji

Unit tests - wady

  • Skomplikowane mockowanie
  • Nie dają pełnej gwarancji
  • Konieczność zmiany podczas refaktoryzacji kodu

Unit tests - jak to działa?

  • Tworzymy klasę dziedziczącą po PHPUnit_Framework_TestCase
  • Klasa powinna być nazwana analogicznie do klasy testowanej, np. dla TextTool tworzymy TextToolTest
  • Wszystkie testy powinny być zawarte w metodach z prefixem "test", np. testExtension
  • Metody testów muszą zawierać asercje, np. assertEquals

Unit tests - przykład

<?php
class MakoTest extends \PHPUnit_Framework_TestCase
{
    public function testDeadline()
    {       
        $expected = 'yesterday';
        $current = 'tomorrow';
        $this->assertEquals($expected, $current);
    }
}

Unit tests - przykład

<?php
use Drupal\makolab\JiraChecker;

class MakoTest extends \PHPUnit_Framework_TestCase
{
    public function testDeadline()
    {       
        $jiraChecker = new JiraChecker();
        $deadline = $jiraChecker->getCurrent()->getDeadline();
        $deadlineTimestamp = $deadline->getTimestamp();
        
        $current = new \DateTime();
        $currentTimestamp = $current->getTimestamp();
        $this->assertLessThan($deadlineTimestamp, $currentTimestamp);
    }
}

Unit tests - @depends

<?php
use Drupal\makolab\JiraChecker;

class MakoTest extends \PHPUnit_Framework_TestCase
{
    public function testDeadline()
    {       
        $jiraChecker = new JiraChecker();
        $deadline = $jiraChecker->getCurrent()->getDeadline();
        $deadlineTimestamp = $deadline->getTimestamp();
        
        $current = new \DateTime();
        $currentTimestamp = $current->getTimestamp();
        $this->assertLessThan($deadlineTimestamp, $currentTimestamp);
        
        return $deadline;
    }
    
    /**
     * @depends testDeadline
     */
    public function testDeadlineBufor($deadline)
    {
        $deadline->modify('+1 day');
        $deadlineTimestamp = $deadline->getTimestamp();
        
        $current = new \DateTime();
        $currentTimestamp = $current->getTimestamp();
        $this->assertLessThan($deadlineTimestamp, $currentTimestamp);
    }
}

Unit tests - @dataProvider

<?php
use Drupal\makolab\JiraChecker;

class MakoTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider projectsProvider
     */
    public function testDeadline($projectName, $closeDate)
    {       
        $jiraChecker = new JiraChecker();
        $deadline = $jiraChecker->getByName($projectName)->getDeadline();
        $deadlineTimestamp = $deadline->getTimestamp();
        
        $closeTimestamp = $closeDate->getTimestamp();
        
        $this->assertLessThan($deadlineTimestamp, $closeTimestamp);
    }
    
    public function projectsProvider()
    {
        return [
            ['paradyz', \DateTime('2017-07-01')],
            ['ecc', \DateTime('2017-10-01')]
        ];
    }
}

Unit tests - ::setUp

<?php
use Drupal\makolab\JiraChecker;

class MakoTest extends \PHPUnit_Framework_TestCase
{
    protected $jiraChecker;
    
    protected function setUp() 
    {
        parent::setUp();

        $this->jiraChecker = new JiraChecker();
    }
    
    public function testDeadline()
    {       
        $deadline = $this->jiraChecker->getCurrent()->getDeadline();
        $deadlineTimestamp = $deadline->getTimestamp();
        
        $current = new \DateTime();
        $currentTimestamp = $current->getTimestamp();
        $this->assertLessThan($deadlineTimestamp, $currentTimestamp);
    }
}

Unit tests - mocking

<?php

class MakoTest extends \PHPUnit_Framework_TestCase
{
    protected $jiraChecker;
    
    protected function setUp() 
    {
        parent::setUp();

        $mockBuilder = $this->getMockBuilder('\Drupal\makolab\Project');
        $projectStub = $mockBuilder->getMock();
        $projectStub->method('getDeadline')->willReturn(new \DateTime());
        
        $mockBuilder = $this->getMockBuilder('\Drupal\makolab\JiraChecker');
        $jiraStub = $mockBuilder->getMock();
        $jiraStub->method('getCurrent')->willReturn($projectStub);
        
        $this->jiraChecker = $jiraStub;
    }
    
    public function testDeadline()
    {       
        $deadline = $this->jiraChecker->getCurrent()->getDeadline();
        $deadlineTimestamp = $deadline->getTimestamp();
        
        $current = new \DateTime();
        $currentTimestamp = $current->getTimestamp();
        $this->assertLessThan($deadlineTimestamp, $currentTimestamp);
    }
}

Unit tests w D8

  • Realizowane za pomocą PHPUnit (4.7 dla D8.3)
  • Lokalizowane w zależności od tego czego dotyczą
  • Każdy test powinien dziedziczyć po Drupal\Tests\UnitTestCase
  • Każdy test powinien przynależeć do grupy określonej za pomocą anotacji @group
  • Wywoływane za pomocą wbudowanego skryptu

Unit tests w D8

<?php
namespace Drupal\Tests\makolab\Unit\Helper;

use Drupal\Tests\UnitTestCase;
use Drupal\makolab\Helper\TextTool;

/**
 * @author piotr.woszczyk
 * @coversDefaultClass \Drupal\makolab\Helper\TextTool
 * @group makolab
 */
class TextToolTest extends UnitTestCase
{
    /**
     * @var \Drupal\makolab\Helper\TextTool
     */
    protected $textTool;
    
    /**
     * {@inheritdoc}
     */
    protected function setUp() {
        parent::setUp();

        $this->textTool = new TextTool();
    }
    
    /**
     * @covers \Drupal\makolab\Helper\TextTool::simplify
     * @dataProvider stringsProvider
     */
    public function testSimplify($actual, $expected)
    {
        $this->assertEquals($expected, $this->textTool->simplify($actual));
    }
    
    public function stringsProvider()
    {
        return [
            ['paRadyz', 'paradyz'],
            [' paraDyż', 'paradyz'],
            ['paradyŻ ', 'paradyz'],
            ['par adyz', 'par adyz'],
            [' par ądyz', 'par adyz'],
            ['par adyz ', 'par adyz'],
            ['PARADYZ', 'paradyz'],
        ];
    }
}

Unit tests w D8

php web/core/scripts/run-tests.sh --module makolab

Drupal test run
---------------

Tests to be run:
  - Drupal\Tests\makolab\Unit\Helper\TextToolTest

Test run started:
  Friday, November 24, 2017 - 04:33

Test summary
------------

Drupal\Tests\makolab\Unit\Helper\TextToolTest                  7 passes

Test run duration: 0 sec

Kernel tests - zalety

  • Testują  zależności pomiędzy elementami
  • Umożliwiają pracę z zewnętrznymi źródłami danych, np. DB
  • Pozwalają dość szybko identyfikować źródło problemu

Kernel tests - wady

  • Są nieco wolniejsze od testów jednostkowych
  • Przygotowanie środowiska potrafi być pracochłonne
  • Nie dają pewności czy użytkownik otrzyma spodziewany efekt

Kernel tests w D8

  • Funkcjonalność specyficzna dla Drupala 8
  • Realizowane za pomocą PHPUnit (4.7 dla D8.3)
  • Lokalizowane w zależności od tego czego dotyczą
  • Każdy test powinien dziedziczyć po Drupal\KernelTests\KernelTestBase
  • Każdy test powinien przynależeć do grupy określonej za pomocą anotacji @group
  • W większości przypadków zawiera @var array $modules
  • Wywoływane za pomocą wbudowanego skryptu

Kernel tests   KernelTestBase::setUp

protected function setUp() {
    parent::setUp();

    $this->root = static::getDrupalRoot();
    $this->initFileCache();
    $this->bootEnvironment();
    $this->bootKernel();
}

Kernel tests - $modules

<?php

class AwardsTest extends KernelTestBase
{
    /**
     * @var array
     */
    public static $modules = ['makolab', 'user', 'simpletest'];
}

Kernel tests - przyklad

<?php

namespace Drupal\Tests\makolab\Kernel\Custom;

use Drupal\KernelTests\KernelTestBase;

/**
 * @author piotr.woszczyk
 * @coversDefaultClass \Drupal\makolab\Custom\Awards
 * @group makolab
 */
class AwardsTest extends KernelTestBase
{
    /**
     * @var array
     */
    public static $modules = ['makolab', 'user', 'simpletest'];
    
    /**
     * @var \Drupal\makolab\Custom\Awards
     */
    protected $awardsService;

    /**
     * {@inheritdoc}
     */
    protected function setUp() 
    {
        parent::setUp();

        $this->awardsService = \Drupal::service('makolab.custom.awards');
    }
    
    /*
     * @covers ::get
     */
    public function testClass()
    {
        $this->assertInstanceOf(
            '\Drupal\makolab\Custom\Awards', 
            $this->awardsService
        );
    }    
}

Bibliografia

  • https://phpunit.de/manual/4.7/en/
  • https://docs.acquia.com/article/lesson-101-unit-and-functional-testing
  • https://www.drupal.org/docs/8/phpunit
  • https://www.drupal.org/docs/8/testing

Powodzenia!

UnitTests in D8

By Piotr Woszczyk

UnitTests in D8

  • 42