Тестування

23/03/2016 0 symfony, розробка

Якщо ви писатимете нову стрічку коду, ви в будь-якому випадку додасте нових багів. Для створення кращих та надійніших додатків, слід тестувати код, використовуючи функціональне та модульне тестування.

Тестовий фреймворк PHPUnit

Для того щоб дати вам потужний фреймворк для тестування, Symfony інтегрована з незалежною бібліотекою, яка називається  PHPUnit. У цьому розділі ми не окреслимо саме PHPUnit, але ви зможете прочитати про це детальніше у документації.

Рекомедовано використовувати найновішу та стабільну версію PHPUnit (ви будете користуватися версією 4.2, або ще кращою, аби протестувати код ядра Symfony).

Кожен тест - незалежно чи він функціональний чи модульний - є PHP класом, що повинен знаходитися у під-директорії  Tests/ ваших пакетів. Якщо ви виконаєте це правило, ви відповідно зможете провести усі тести для додатку скориставшись наступною командою:

# specify the configuration directory on the command line
$ phpunit -c app/

Опція -c дає команду PHPUnit подивитися в директорію (каталог) app/, щоб знайти файл з налаштуваннями. Якщо ви зацікавлені у опціях PHPUnit, подивіться файл app/phpunit.xml.dist.

Покриття коду генерується опцією --coverage-*, більше інформації можна побачити скориставшись опцією --help.  

Модульні тести

Модульний тест - це тест для одного PHP класу, який називається модуль. Якщо ви хочете протестувати ваш додаток повністю, прочитайте підрозділ Функціональне тестування.

Написання модульних тестів для Symfony нічим не відрізняється від написання стандартних модульних тестів для PHPUnit. Уявімо, що у нас є дуже простий клас під назвою Calculator у директорії Util/  пакету додатка:

// src/AppBundle/Util/Calculator.php
namespace AppBundle\Util;

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Для того, аби протестувати це, створіть файл CalculatorTest у директорії Tests/Util вашого пакета:

// src/AppBundle/Tests/Util/CalculatorTest.php
namespace AppBundle\Tests\Util;

use AppBundle\Util\Calculator;

class CalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testAdd()
    {
        $calc = new Calculator();
        $result = $calc->add(30, 12);

        // assert that your calculator added the numbers correctly!
        $this->assertEquals(42, $result);
    }
}

Умовно, під-директорія Tests/ повинна копіювати директорію вашого пакета для модульного тестування. Отож, якщо ви тестуєте клас у директорії Util/ вашого пакета, помістіть тест у директорію Tests/Util/.

Так само як і у вашому справжньому додатку - автозавантажування відбувається автоматично через файл bootstrap.php.cache (тому що він налаштований за замовчуванням у файлі app/phpunit.xml.dist)  

Провести такий тест для даного файлу з директорії досить просто:

# run all tests of the application
$ phpunit -c app

# run all tests in the Util directory
$ phpunit -c app src/AppBundle/Tests/Util

# run tests for the Calculator class
$ phpunit -c app src/AppBundle/Tests/Util/CalculatorTest.php

# run all tests for the entire Bundle
$ phpunit -c app src/AppBundle/

Функціональне тестування

Функціональне тестування перевіряє інтеграцію різних рівнів додатку (починаючи від маршрутизації і закінчуючи переглядом). Це тестування нічим не відрізняється від модульного тестування коли це стосується PHPUnit, проте це тестування має специфічний робочий процес:

  1. створіть запит;

  2. протестуйте відповідь;

  3. клацніть на лінк і відправте заповнену форму;

  4. знову протестуйте відповідь;

  5. передивіться і повторіть.

Створюємо перший Функціональний тест

Функціональні тести - це прості PHP файли, які за звичай розміщені у директорії Tests/Controller вашого пакету. Якщо ви хочете протестувати сторінки, які підтримуються класом PostController, розпочніть зі створення нового файлу PostControllerTest.php, який розширить функціонал класу WebTestCase.

Ось так на прикладі виглядає цей тест:

 // src/AppBundle/Tests/Controller/PostControllerTest.php
namespace AppBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class PostControllerTest extends WebTestCase
{
    public function testShowPost()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/post/hello-world');

        $this->assertGreaterThan(
            0,
            $crawler->filter('html:contains("Hello World")')->count()
        );
    }
}

Для того, аби запустити функціональне тестування, клас WebTestCase завантажує ядро вашого додатку. У більшості випадків, це відбувається автоматично. Однак, якщо ядро знаходиться у нестандартному каталозі, вам буде потрібно змінити файл phpunit.xml.dist для того, щоб встановити змінну середовища KERNEL_DIR до директорії вашого ядра:

 

<?xml version="1.0" charset="utf-8" ?>
<phpunit>
    <php>
        <server name="KERNEL_DIR" value="/path/to/your/app/" />
    </php>
    <!-- ... -->
</phpunit>

Метод createClient() повертає клієнта, який нагадує браузер, за допомогою якого ви заходите на вебсайт:

$crawler = $client->request('GET', '/post/hello-world');

Метод request() (детальніше про цей метод) повертає об’єкт Crawler, який використовується для вибірки елементів у відповідь, просто клацніть на посилання і відішліть заповнені форми.

Метод Crawler працює лише тоді, коли відповіддю є документи XML або HTML. Щоб отримати просту контентну відповідь, задайте таку команду:$client->getResponse()->getContent().

Натисніть на посилання вибравши його за допомогою виразу XPath або CSS селектора, а потім скористуйтесь клієнтом для натиснення. Наприклад:

$link = $crawler
    ->filter('a:contains("Greet")') // find all links with the text "Greet"
    ->eq(1) // select the second link in the list
    ->link()
;

// and click it
$crawler = $client->click($link);

Майже ідентичний процес відбувається під час відправки форми. Спочатку оберіть кнопку форми, перевизначіть деякі її функції і потім надсилайте відповідну форму:

$form = $crawler->selectButton('submit')->form();

// set some values
$form['name'] = 'Lucas';
$form['form_name[subject]'] = 'Hey there!';

// submit the form
$crawler = $client->submit($form);

Форма також може обробляти завантаження і містити методи для заповнення різних типів полів форми. (наприклад, select() та tick()). Більше деталей ви прочитаєте у підрозділі нижче.

Тепер, коли ви легко можете переміщатися по усьому додатку, використовуйте припущення (assertions), аби протестувати, чи справді він працює як належить. Використовуйте Crawler щоб зробити припущення (assertions) у  об’єктнїй моделі документів (DOM):

// Assert that the response matches a given CSS selector.
$this->assertGreaterThan(0, $crawler->filter('h1')->count());

Або напряму протестуйте зміст відповіді, якщо ви просто хочете затвердити, що контент містить деякий текст, або на всяк випадок, що відповідь не є документами з розширенням XML або HTML:

$this->assertContains(
    'Hello World',
    $client->getResponse()->getContent()
);

Корисні припущення (аssertions)

Щоб працювати швидше, зверніть увагу на список припущень, які вживаються найчастіше:

use Symfony\Component\HttpFoundation\Response;

// ...

// Assert that there is at least one h2 tag
// with the class "subtitle"
$this->assertGreaterThan(
    0,
    $crawler->filter('h2.subtitle')->count()
);

// Assert that there are exactly 4 h2 tags on the page
$this->assertCount(4, $crawler->filter('h2'));

// Assert that the "Content-Type" header is "application/json"
$this->assertTrue(
    $client->getResponse()->headers->contains(
        'Content-Type',
        'application/json'
    )
);

// Assert that the response content contains a string
$this->assertContains('foo', $client->getResponse()->getContent());
// ...or matches a regex
$this->assertRegExp('/foo(bar)?/', $client->getResponse()->getContent());

// Assert that the response status code is 2xx
$this->assertTrue($client->getResponse()->isSuccessful());
// Assert that the response status code is 404
$this->assertTrue($client->getResponse()->isNotFound());
// Assert a specific 200 status code
$this->assertEquals(
    200, // or Symfony\Component\HttpFoundation\Response::HTTP_OK
    $client->getResponse()->getStatusCode()
);

// Assert that the response is a redirect to /demo/contact
$this->assertTrue(
    $client->getResponse()->isRedirect('/demo/contact')
);
// ...or simply check that the response is a redirect to any URL
$this->assertTrue($client->getResponse()->isRedirect());

Робота з тестовим клієнтом

Тестовий клієнт симулює клієнта  HTTP, так як це робить браузер, і подає запити до додатка  Symfony:

$crawler = $client->request('GET', '/post/hello-world');

Метод request() бере метод HTTP та URL як аргументи, і повертає об’єкт Crawler.

Хардкодити запит URL-ів - найкраща практика для функціональних тестів. Якщо тест генерує URL використовуючи маршрутизатор Symfony, то він не визначить ніяких змін зроблених URL- ам додатку, які можуть вплинути на користувачів.  

Детальніше про метод request():

Повністю цей метод виглядає ось так:  

request(
    $method,
    $uri,
    array $parameters = array(),
    array $files = array(),
    array $server = array(),
    $content = null,
    $changeHistory = true
)

Масив server - це масив не оброблених значень, які ви зазвичай знайдете у PHP $_SERVER. Наприклад, щоб встановити хедери Content-Type, Referer та X-Requested-With HTTP, зробіть наступне (зверніть увагу на префікс HTTP_ для не стандартних хедерів):

$client->request(
    'GET',
    '/post/hello-world',
    array(),
    array(),
    array(
        'CONTENT_TYPE'          => 'application/json',
        'HTTP_REFERER'          => '/foo/bar',
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    )
);

Використовуйте пошуковий робот, щоб знайти елементи DOM у відповіді. Ці елементи можуть використовуватися для того щоб натискати на лінк та відсилати форми:

$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);

$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Fabien'));

Обидва методи click() та submit() повертають об’єкт Crawler. Ці методи - це найкращий спосіб переглянути ваш додаток, оскільки він піклується про багато речей, наприклад видаляє метод HTTP з форми і дає вам гарний API для завантаження файлів.  

Ви дізнаєтеся більше про об’єкти Link та Form  у секції Crawler.

Метод request може використовуватися безпосередньо для імітації форми подання або виконувати більш складні запити. А ось декілька корисних прикладів:  

// Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Fabien'));

// Submit a raw JSON string in the request body
$client->request(
    'POST',
    '/submit',
    array(),
    array(),
    array('CONTENT_TYPE' => 'application/json'),
    '{"name":"Fabien"}'
);

// Form submission with a file upload
use Symfony\Component\HttpFoundation\File\UploadedFile;

$photo = new UploadedFile(
    '/path/to/photo.jpg',
    'photo.jpg',
    'image/jpeg',
    123
);
$client->request(
    'POST',
    '/submit',
    array('name' => 'Fabien'),
    array('photo' => $photo)
);

// Perform a DELETE request and pass HTTP headers
$client->request(
    'DELETE',
    '/post/12',
    array(),
    array(),
    array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
);

І останнім, проте не менш важливим є те, що ви можете ‘змусити’  кожен запит у PHP процесі виконатися, щоб уникнути будь-яких побічних ефектів під час роботи з декількома клієнтами у тому самому скрипті:

$client->insulate();

Браузинг

Клієнт підтримує велику кількість операцій, які можуть виконуватися просто у браузері:

$client->back();
$client->forward();
$client->reload();

// Clears all cookies and the history
$client->restart();

Доступ до внутрішніх об’єктів

Якщо ви користуєтеся клієнтом для того, аби протестувати ваш додаток, можливо ви захочете отримати доступ до внутрішніх об’єктів клієнта:

$history = $client->getHistory();
$cookieJar = $client->getCookieJar();

Також, в можете отримати доступ до об’єктів, що стосуються останнього запиту:  

// the HttpKernel request instance
$request = $client->getRequest();

// the BrowserKit request instance
$request = $client->getInternalRequest();

// the HttpKernel response instance
$response = $client->getResponse();

// the BrowserKit response instance
$response = $client->getInternalResponse();

$crawler = $client->getCrawler();

Якщо ваші запити не ізольовані, ви також можете отримати доступ до Container та Kernel:

$container = $client->getContainer();
$kernel = $client->getKernel();

Доступ до контейнера

Рекомендовано, щоб функціональний тест тестував тільки об’єкт Response. Але під впливом певних, але не частих випадків, вам буде потрібно отримати доступ до деяких внутрішніх об’єктів, для того щоб створити припущення (assertions). У таких випадках ви матимете доступ до контейнера впровадження наслідування (Dependency Injection Container):

$container = $client->getContainer();

Проте зауважте, що все це не буде працювати, якщо ви ізолюєте клієнта, або якщо ви використовуватимете шар HTTP. Щоб отримати список послуг, доступних у додатку, використовуйте завдання у консолі, debug:container.

Якщо інформація, яку вам потрібно перевірити, доступна з профайлу, тоді ви можете використати її натомість.

Доступ до даних профайлера

На кожен запит, ви можете дати команду профайлеру Symfony збирати дані про внутрішню роботу запиту. Наприклад, профайлер може використовуватися для верифікації, чи дана сторінка виконує менше ніж потрібно від кількості запитів до бази даних під час завантаження.  


Щоб отримати останній запит профайлера, зробіть наступне:

// enable the profiler for the very next request
$client->enableProfiler();

$crawler = $client->request('GET', '/profiler');

// get the profile
$profile = $client->getProfile();

Більше інформації про особливі деталі користування профайлелом всередині тесту, ви можете прочитати у довіднику: How to Use the Profiler in a Functional Test.

Перенаправлення

Коли запит повертає відповідь перенаправлення, клієнт не слідує йому автоматично. Ви можете перевірити відповідь, і потім зробити перенаправлення за допомогою методу followRedirect():

$crawler = $client->followRedirect();

Якщо ви хочете, щоб клієнт слідував усім перенаправленням автоматично, використайте метод followRedirects():

$client->followRedirects();

Якщо ви дасте значення false методові followRedirects(), тоді він не буде слідувати перенаправленням:

$client->followRedirects(false);

Пошуковий робот (Crawler)

Примірник пошукового робота повертається кожен раз, коли ви робите запит за допомогою клієнта. Це дозволить вам перетинати HTML документи, обирати вузли, знаходити посилання та форми.  

Переміщення

Так само як і jQuery, пошуковий робот містить методи для переміщення по об’єктній моделі документів  (DOM) у документах HTML/XML. Kод у наступному прикладі, знаходить усі елементи input[type=submit], вибирає останній елемент на сторінці, а потім вибирає свій безпосередній батьківський елемент:

$newCrawler = $crawler->filter('input[type=submit]')
    ->last()
    ->parents()
    ->first()
;

Використовуйте усі наступні методи також:

filter('h1.title')

Ноди, які відповідають за селектор CSS.

filterXpath('h1')

Ноди, які збігаються з виразом XPath.

eq(1)

Нод для зазначеного індексу.

first()

Перший нод.

last()

Останній нод.

siblings()

Ноди спільного батьківського елемента (вузла).

nextAll()

Усі наступні ноди спільного батьківського елемента (вузла).

previousAll()

Усі попередні ноди спільного батьківського елемента (вузла).

parents()

Повернення до батьківського елемента.  

children()

Повернення до дочірнього елемента.  

reduce($lambda)

Ноди, дя якийх функція не повертає false.

Оскільки кожен з наступних методів повертає новий екземпляр пошукового робота (Crawler), ви можете звузити вибір нодів за допомогою ланцюжка викликів методів:

$crawler
    ->filter('h1')
    ->reduce(function ($node, $i) {
        if (!$node->getAttribute('class')) {
            return false;
        }
    })
    ->first()
;

Скористайтеся функцією count() для того, щоб отримати кількість вузлів, які зберігаються у crawler: count($crawler).  

Добуваємо інформацію

Пошуковий робот може добувати інформацію з нодів:  

// Returns the attribute value for the first node
$crawler->attr('class');

// Returns the node value for the first node
$crawler->text();

// Extracts an array of attributes for all nodes
// (_text returns the node value)
// returns an array for each element in crawler,
// each with the value and href
$info = $crawler->extract(array('_text', 'href'));

// Executes a lambda for each node and return an array of results
$data = $crawler->each(function ($node, $i) {
    return $node->attr('href');
});

Посилання

Для вибірки посилань використовуйте згаданий вище метод претинання, або зручну команду selectLink():

 $crawler->selectLink('Click here');

Це зробить вибірку усіх посилань, які вміщатимуть текст, що в дужках, або зображення, на які можна натиснути, де тег alt вміщатиме даний текст. Як і інші методи фільтрації, це повертатиме інший об’єкт Crawler.  

Коли ви виберете лінк, у вас з’явиться доступ до спеціального об’єкта Link, який містить допоміжні методи до спеціальних посилань (наприклад, getMethod() та getUri()). Щоб натиснути на посилання, використайте метод click() і передайте це об’ктові Link.

$link = $crawler->selectLink('Click here')->link();

$client->click($link);

Форми

Ви можете обирати форми, за допомогою кнопок, які можна вибрати методом selectButton(), так само як і посилання:  

$buttonCrawlerNode = $crawler->selectButton('submit');

Зверніть увагу, що ви обираєте кнопки форм, а не просто форми, оскільки форма може містити декілька кнопок; якщо ви користуєтесь API (прикладним програмним інтерфейсом),  пам’ятайте, що вам слід обирати просто кнопку.  

Метод selectButton() може обирати теги button і представляти вже введені теги. Він використовує декілька частин кнопок, щоб їх знайти:  

  • атрибут value для значення;

  • атрибути id або alt використовуються для зображень;  

  • атрибути id або name використовуються для тегів кнопок.

Якщо у вас є пошуковий робот, який представляє кнопки, використайте метод form() щоб отримати екземпляр Form для для форми, яка охоплює вузол кнопки.

$form = $buttonCrawlerNode->form();

При використанні методу form() ви також помітите масив значень полів, що скасовує ті, що за замовчуванням.

 

$form = $buttonCrawlerNode->form(array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

I якщо у вас виникне бажання імітувати специфічний HTTP метод для форми, використайте наступний аргумент:  

$form = $buttonCrawlerNode->form(array(), 'DELETE');

Клієнт може відправити екземпляри Form:

$client->submit($form);

Значення полів, можуть використовуватися у якості другого аргументу для методу submit().  

$client->submit($form, array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

У складніших випадках, у нагоді стане екземпляр Form, як масив, який поставить значення кожного поля індивідуально:  

// Change the value of a field
$form['name'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

Існує також і хороший API для маніпуляції значень полів згідно з їхнім типом:  

 

// Select an option or a radio
$form['country']->select('France');

// Tick a checkbox
$form['like_symfony']->tick();

// Upload a file
$form['photo']->upload('/path/to/lucas.jpg');

Якщо ви бажаєте обрати значення "invalid", дивіться розділ Selecting Invalid Choice Values.

Ви також можете отримати значення, які будуть надіслані за допомогою методу getValues() і об’єкта Form. Завантажені файли доступні у окремому масиві повертаються методом getFiles(). Методи getPhpValues() та getPhpFiles() також повертають відіслані значення, проте у форматі PHP (конвертує ключі з квадратними дужками, наприклад  my_form[subject] - до масиву PHP).

Налаштування тестування

Клієнт, який використовується функціональними тестами, створює ядро, яке запускається у спеціальному середовищі test. Оскільки Symfony завантажує app/config/config_test.yml у середовищі test, ви можете використати будь які з налаштувань вашого додатку спеціально для тестування.

Наприклад, по замовчуванню Swift Mailer налаштований не для надсилання повідомлень у середовищі тестування. Це ви можете прослідкувати у опції налаштування swiftmailer:

YAML

 

# app/config/config_test.yml

# ...
swiftmailer:
    disable_delivery: true

XML

 

<!-- app/config/config_test.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/swiftmailer
        http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">

    <!-- ... -->
    <swiftmailer:config disable-delivery="true" />
</container>

PHP

// app/config/config_test.php

// ...
$container->loadFromExtension('swiftmailer', array(
    'disable_delivery' => true,
));

Ви також можете використати повністю інше середовище, або змінити режим за замовчуванням (true), використавши метод createClient():  

$client = static::createClient(array(
    'environment' => 'my_test_env',
    'debug'       => false,
));

Якщо ваш додаток поводиться згідно деяких заголовків HTTP, подайте їх у якості другого аргументу createClient():

$client = static::createClient(array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

Ви також можете змінити заголовки HTTP згідно запиту:  

$client->request('GET', '/', array(), array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

Тестовий клієнт доступний як сервіс у контейнері і середовищі test (або будь-де, де доступна опція framework.test ). Це означає що ви можете повністю змінити сервіс, якщо потрібно.  

Налаштування PHPUnit

Кожен додаток має свої налаштування PHPUnit, що зберігаються у файлі phpunit.xml.dist. Ви можете редагувати цей файл, щоб змінити налаштування за замовчуванням, або створити файл phpunit.xml, для створення конфігурації для вашого локального комп’ютера.  

Зберігайте файл phpunit.xml.dist у сховищі вашого коду, та ігноруйте файл phpunit.xml.  

За замовчуванням, лише ті тести, які зберігаються у /tests запускаються за допомогою команди phpunit, що налаштовується через файл:

<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <!-- ... -->
</phpunit>

Проте, ви легко можете додати й інші директорії (каталоги). Наприклад, наступне налаштування додає тести з користувацької директорії (каталогу)  lib/tests:

<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <!-- ... --->
            <directory>../lib/tests</directory>
        </testsuite>
    </testsuites>
    <!-- ... --->
</phpunit>

Щоб залучити інші директорії у код, редагуйте секцію <filter>:  

<!-- phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <filter>
        <whitelist>
            <!-- ... -->
            <directory>../lib</directory>
            <exclude>
                <!-- ... -->
                <directory>../lib/tests</directory>
            </exclude>
        </whitelist>
    </filter>
    <!-- ... --->
</phpunit>

Дізнайтеся більше

Розділ про тести у фреймворці Symfony

Компонент DomCrawler

Компонент CssSelector

Як імітувати аутентифікацію HTTP у функціональному тесті

Як протестувати взаємодію декількох клієнтів

Як використати профайлер у функціональному тесті

Як налаштувати процес початкового завантаження перед запуском тестування

Поділитися