Тестирование

Что такое тестирование?

Тести́рование програ́ммного обеспе́че́ния — процесс исследования, испытания программного продукта, имеющий две различные цели:

  • выявить ситуации, в которых поведение программы является неправильным, нежелательным или не соответствующим спецификации.

 

  • продемонстрировать разработчикам и заказчикам, что программа соответствует требованиям;

Что у нас есть?

CODECEPTION 2.1

CODECEPTION 2.1



"require": {
    "php": ">=5.4.0",
    "ext-json": "*",
    "ext-mbstring": "*",
    "phpunit/phpunit": "~4.8.0",
    "facebook/webdriver": ">=1.0.1",
    "guzzlehttp/guzzle": ">=4.1.4 <7.0",
    "guzzlehttp/psr7": "~1.0",
    "symfony/finder": "~2.4",
    "symfony/console": "~2.4",
    "symfony/event-dispatcher": "~2.4",
    "symfony/yaml": "~2.4",
    "symfony/browser-kit": "~2.4",
    "symfony/css-selector": "~2.4",
    "symfony/dom-crawler": "~2.4,!=2.4.5"
}

CODECEPTION 2.1

CODECEPTION 2.1

Быстрый старт


// 1. Скачиваем Codeception
php composer.phar require "codeception/codeception:*"

// 2. Инициализируем Codeception, создаются файл codeception.yml и tests директория
php codecept bootstrap

Какие типы тестирования мы используем?

Aceptence тесты

Суть
    - Проверка работы приложения в реальном браузере
    - Реальные HTTP-запросы к сайту

Достоинства

    - Реальный браузер
    - JavaSript & CSS
    - Для любых сайтов на любом ЯП

Недостатки
    - Медленные
    - Нужен запущенный сайт
    - Нужен Selenium Server

Aceptence тесты

// 1. Создаем acceptance тест (tests/acceptance/WelcomeCept.php)
php codecept.phar generate:cept acceptance Welcome

// 2. Пишем тест
$I = new AcceptanceTester($scenario);
$I->wantTo('sign in');
$I->amOnPage('/login');
$I->fillField('username', 'davert');
$I->fillField('password', 'qwerty');
$I->click('LOGIN');
$I->see('Welcome, Davert!');

// 3. Настраиваем Acceptance тесты (tests/acceptance.suite.yml)
class_name: AcceptanceTester
modules:
    enabled:
        - PhpBrowser:
            url: {YOUR APP URL}
        - \Helper\AcceptanceTester

// 4. Запускаем тест
php codecept.phar run acceptance

Aceptence


$I = new AcceptanceTester($scenario);
$I->wantTo('sign in');
$I->amOnPage('/login');
$I->fillField('username', 'davert');
$I->fillField('password', 'qwerty');
$I->click('LOGIN');
$I->see('Welcome, Davert!');

// Легко трансформируем в:

I WANT TO SIGN IN
I am on page '/login'
I fill field 'username', 'davert'
I fill field 'password', 'qwerty'
I click 'LOGIN'
I see 'Welcome, Davert!'

// Вот так:
$ php codecept.phar generate:scenarios

(простой сценарий)

Aceptence

// Click, Emulates a click on valid anchors. CSS or XPath selector.
$I->click('Log in');         
$I->click('#login a');       // CSS selector applied
$I->click('//a[@id=login]'); // XPath
$I->click('Login', '.nav');  // Using context as second argument

// Forms
$I->fillField('Name', 'Miles');
$I->fillField('user[email]','miles@davis.com');
$I->selectOption('Gender','Male');
$I->click('Update');

// Assertions
$I->see('Thank you, Miles'); // Check that 'Thank you, Miles' is on page.
$I->see('Thank you', '.notice'); // Check that 'Thank you' is inside the element with 'notice' class.
$I->dontSee('Form is filled incorrectly'); // Check this message is not on page.

// Conditional Assertions
$I->canSeeInCurrentUrl('/user/miles');
$I->canSeeCheckboxIsChecked('#agree');
$I->cantSeeInField('user[name]', 'Miles');

// Cookies, Urls, Title, etc
$I->setCookie('auth', '123345');
$I->seeCookie('auth');

(PHP Browser)

  • Вы можете кликнуть только по ссылкам с валидным url по кнопке которая сабмитит форму

  • Вы не можете заполнить поля, которые не в форме

  • Вы не можете работать с JavaScript взаимодействием: modal windows, datepickers, и т. д.

Aceptence


// Вижу
$I->seeElement('#modal');

// Ожидаю
$I->wait(3);
$I->waitForElement('#agree_button', 30); // secs
$I->click('#agree_button');

// Session Snapshots - постоянная сессия пользователя между тестами.

// Multi Session Testing - когда вам необходимо использовать два браузера
// в одно время для одних и тех же тестов.

// Если ошибка, то сохраняется html код страницы и делается скрин браузера

(Selenium WebDriver)

Aceptence


class_name: AcceptanceTester
modules:
    enabled:
        - WebDriver
        - tests\codeception\_support\FixtureHelper
    config:
        WebDriver:
            url: http://localhost:8080
            browser: firefox
            restart: true
            window_size: 1024x768
env:
    screen:
    mobile:
        modules:
            config:
                WebDriver:
                    window_size: 320x240

(Settings)


<?php

namespace tests\acceptance;

use \AcceptanceTester;

class SearchCest {

   /**
    * @env mobile
    */
    public function tryToTest(AcceptanceTester $I)
    {
        $I->amOnPage('/');
    }
}

Functional тесты

Суть
    - Проверка контроллеров, HTML, HTTP
    - Отсутствие запросов к реальному сайту
    - Заполнение GET, POST, SERVER
    - Многократный запуск приложения в самой консоли


Достоинства

    - Без запущенного сайта
    - Быстрее

Сложности

    - Отсутствие JavaScript
    - Необходимость модуля для фреймворка
    - Выполнение последовательно в одном потоке
        - Необходимость перезапускать приложения
        - Конфликты HTTP-заголовков
        - Выход по die() и exit()
        - Глобальные переменные и статические поля

Functional тесты

// 1. Создаем functional тест (tests/functional/WelcomeFunc.php)
php codecept.phar generate:cept functional Welcome

// 2. Пишем тест
<?php
$I = new FunctionalTester($scenario);
$I->amOnPage('/');
$I->click('Login');
$I->fillField('Username', 'Miles');
$I->fillField('Password', 'Davis');
$I->click('Enter');
$I->see('Hello, Miles', 'h1');
?>

// 3. Настраиваем Functional тесты (tests/functional.suite.yml)
class_name: FunctionalTester
modules:
    enabled:
        - Filesystem
        - Yii2
    config:
        Yii2:
            configFile: 'codeception/config/functional.php'

// 4. Запускаем тест
php codecept.phar run functional

Функциональные тесты практически такие же, как и функциональные, за одним исключительным моментом: функциональные тесты не требуют web сервер для запуска тестов.

Если просто, то мы устанавливаем $_REQUEST, $_GET и $_POST переменные а затем мы запускаем приложение из теста.

Functional тесты

Acceptance тесты в основном значительно медленнее чем функциональные тесты. Но функциональные тесты менее стабильны если запустить Codeception и приложение в одном окружении. Если в вашем приложении вы используете оператор exit или глобальные переменные, то возможно функциональные тесты не для вас.

 

Headers, Cookies, Sessions

Один из распространенных вопросов, с функциональными тестами является использование PHP функций, которые имеют дело с заголовками, сессиями и куками. Как вы знаете, функции заголовков вызывают ошибки, если она выполняется более чем один раз для того же заголовка. В функциональных тестах мы запускаем приложение несколько раз, таким образом, в результате мы получим много ошибок.

 

Shared Memory (общая память)

В отличие от функционального тестирования традиционным способом, PHP приложение не останавливается после того, как завершит обработку запроса. Так как все запросы выполняются в одном контейнере памяти, они не являются изолированными. Итак, если вы видите, что ваши тесты загадочно фейлятся, когда они не должны - попытайтесь выполнить их по одному. Что бы проверить, были ли тесты изолированы во время работы. Потому что очень легко испортить окружение, если все тесты выполняются в общей памяти. Держите вашу память чистой, что бы избежать утечек памяти а так же чистые глобальные и статические переменные.

(ловушки)

Unit тесты

Text

Суть

    - Только один класс
    - Заглушки вместо зависимых объектов
    - Без работы с БД
    - Без работы с файлами

 

Достоинства

    - Без перезапуска приложения
    - Очень быстрые

 

Недостатки

    - Не тестируют взаимосвязи
    - Возможная некорректность заглушек

(Модульные)

Text

Суть
    - Тестирование связанных компонентов
    - Работа с БД и другими ресурсами
    - Заглушки только для критичных вещей

Достоинства

    - Без перезапуска приложения
    - Мало заглушек
    - Быстрые

Недостатки

    - Не тестируют контроллеры

Unit тесты

(Интеграционные)

Unit тесты

(Подходы)

Text

TDD (test-driven development) - разработка через тестирование. Техника разработки программного обеспечения, которая основывается на повторении очень коротких циклов разработки: сначала пишется тест, покрывающий желаемое изменение, затем пишется код, который позволит пройти тест, и под конец проводится рефакторинг нового кода к соответствующим стандартам.

BDD (behavior-driven development) - разработка, основанная на функционировании, фокусируемся на том, как программа работает, а не на том, что она производит в конечном итоге. Мышление в терминах функциональности (того, что код должен делать), приводит к подходу, когда сначала пишутся классы для проверки спецификации, которые, в свою очередь, могут оказаться очень эффективным инструментом реализации.

TDD — делать вещи правильно. BDD — делать правильные вещи.

Unit тесты

// 1. Создаем unit тест (tests/unit/ExampleTest.php)
php codecept.phar generate:test unit Example

// 2. Пишем тест
class ExampleTest extends \Codeception\TestCase\Test
{
    protected $tester;

    // executed before each test like setUp in PHPUnit
    protected function _before() {}

    // executed after each test like tearDown in PHPUnit
    protected function _after() {}

    public function testValidation()
    {
        $user = User::create();

        $user->username = null;
        $this->assertFalse($user->validate(['username']));

        $user->username = 'toolooooongnaaaaaaameeee';
        $this->assertFalse($user->validate(['username']));

        $user->username = 'davert';
        $this->assertTrue($user->validate(['username']));           
    }
}
// 3. Настраиваем Unit тесты
// (tests/unit.suite.yml)
class_name: UnitTester
modules:
    enabled: 
        - Asserts
        - \Helper\Unit

// 4. Запускаем тест
php codecept.phar run

Unit тесты

(Specify BDD style code)

Text

class UserTest extends PHPUnit_Framework_TestCase {

    use Codeception\Specify;

    public function setUp() {       
        $this->user = new User;
    }

    public function testValidation() {
        $this->assertInstanceOf('Model', $this->user);

        $this->specify("username is required", function() {
            $this->user->username = null;
            verify($user->validate(['username'])->false()); 
        });

        $this->specify("username is too long", function() {
            $this->user->username = 'toolooooongnaaaaaaameeee',
            verify($user->validate(['username'])->false());         
        });

        // alternative, TDD assertions can be used too.
        $this->specify("username is ok", function() {
            $this->user->username = 'davert',
            $this->assertTrue($user->validate(['username']));           
        });             
    }
}

verify($user->getName())->equals('john'); // Verify for BDD-style assertions

Unit тесты

Testing Database

Text

function testSavingUser()
{
    $user = new User();
    $user->setName('Miles');
    $user->setSurname('Davis');
    $user->save();
    $this->assertEquals('Miles Davis', $user->getFullName());
    $this->tester->seeInDatabase('users', array('name' => 'Miles', 'surname' => 'Davis'));
}

Accessing Module

/** @var Doctrine\ORM\EntityManager */
$em = $this->getModule('Doctrine2')->em;

Stubs

$user = Stub::make('User', ['getName' => 'john']);
$name = $user->getName(); // 'john'

Unit тесты

Data provider

Text

<?php
class DataTest extends Test
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertEquals($expected, $a + $b);
    }

    public function additionProvider()
    {
        return array(
          array(0, 0, 0),
          array(0, 1, 1),
          array(1, 0, 1),
          array(1, 1, 3)
        );
    }
}

Unit тесты

Начало и завершение 

Text

<?php
use Codeception\Util\Stub;

class ExampleTest extends \Codeception\TestCase\Test
{
   /**
    * @var UnitTester
    */
    protected $tester;

    // executed before each test
    // setUp() method in phpUnit
    protected function _before()
    {
    }

    // executed after each test
    // tearDown() method in phpUnit
    protected function _after()
    {
    }
}
?>

Детальней

Ну как, понятно?

Поддерживаемые модули


* AMQP
* Phalcon2
* Asserts
* PhpBrowser
* Cli
* Queue
* Db
* REST
* Dbh
* Redis
* Doctrine2
* SOAP

* FTP
* Sequence
* Facebook
* Silex
* Filesystem
* Symfony2
* Laravel4
* WebDriver
* Laravel5
* XMLRPC
* Lumen
* Yii1

* Yii1
* Memcache
* Yii2
* MongoDb
* ZF1
* Phalcon1
* ZF2
// grabMessageFromQueue, takes last message from queue.
$message = $I->grabMessageFromQueue('queue.emails');

// param $queue
// return AMQPMessage
// ----------------------------------------

// pushToExchange, Sends message to exchange by sending exchange name, message and a routing key

$I->pushToExchange('exchange.emails', 'thanks');
$I->pushToExchange('exchange.emails', new AMQPMessage('Thanks!'));
$I->pushToExchange('exchange.emails', new AMQPMessage('Thanks!'), 'severity');

// param $exchange, $message string|AMQPMessage, $routing_key
// ----------------------------------------

// pushToQueue, sends message to queue

$I->pushToQueue('queue.jobs', 'create user');
$I->pushToQueue('queue.jobs', new AMQPMessage('create'));

// param $queue, $message string|AMQPMessage
// ----------------------------------------

// seeMessageInQueueContainsText checks if message containing text received.

$I->pushToQueue('queue.emails', 'Hello, davert');
$I->seeMessageInQueueContainsText('queue.emails','davert');
// param $queue, $text

AMQP


assertContains - Checks that haystack contains needle
assertEmpty - Checks that variable is empty.
assertEquals - Checks that two variables are equal.
assertFalse - Checks that condition is negative.
assertFileExists - Checks if file exists
assertFileNotExists - Checks if file doesn't exist
assertGreaterThan - Checks that actual is greater than expected
assertGreaterThanOrEqual - Checks that actual is greater or equal than expected
assertLessThan - Checks that actual is less than expected
assertLessThanOrEqual - Checks that actual is less or equal than expected
assertNotContains - Checks that haystack doesn't contain needle.
assertNotEmpty - Checks that variable is not empty.
assertNotEquals - Checks that two variables are not equal
assertNotNull - Checks that variable is not NULL
assertNotRegExp - Checks that string not match with pattern
assertNotSame - Checks that two variables are not same
assertNull - Checks that variable is NULL
assertRegExp - Checks that string match with pattern
assertSame - Checks that two variables are same
assertTrue - Checks that condition is positive.
fail - Fails the test with message.

Asserts


// dontSeeInDatabase, asserts that there is no record with the given column values in a database.
// Effect is opposite to ->seeInDatabase

$I->dontSeeInDatabase('users', array('name' => 'Davert', 'email' => 'davert * `mail.com'));`
// Will generate:
SELECT COUNT(*) FROM `users` WHERE `name` = 'Davert' AND `email` = 'davert * `mail.com'`
// ---------------------------------------------------------------------------

//grabFromDatabase, fetches a single column value from a database.

$mail = $I->grabFromDatabase('users', 'email', array('name' => 'Davert'));
// ---------------------------------------------------------------------------

// haveInDatabase, inserts an SQL record into a database.

$I->haveInDatabase('users', array('name' => 'miles', 'email' => 'miles * `davis.com'));
// ---------------------------------------------------------------------------

// seeNumRecords, asserts that the given number of records were found in the database.

$I->seeNumRecords(1, 'users', ['name' => 'davert'])

DB

// cleanDir, erases directory contents
$I->cleanDir('logs');

// copyDir, Copies directory with all contents
$I->copyDir('vendor','old_vendor');

// deleteDir, Deletes directory with all subdirectories
$I->deleteDir('vendor');

// deleteFile, Deletes a file
$I->deleteFile('composer.lock');

// dontSeeInThisFile, Checks If opened file doesn't contain text in it
$I->openFile('composer.json');
$I->dontSeeInThisFile('codeception/codeception');

// openFile, Opens a file and stores it's content.
$I->openFile('composer.json');
$I->seeInThisFile('codeception/codeception');

// seeFileContentsEqual, Checks the strict matching of file contents.
$I->openFile('process.pid');
$I->seeFileContentsEqual('3192');

// seeFileFound, Checks if file exists in path. Opens a file when it's exists
$I->seeFileFound('UserModel.php','app/models');

// seeInThisFile, Checks If opened file has text in it.
$I->openFile('composer.json');
$I->seeInThisFile('codeception/codeception');

Filesystem

// Разные виды аутентификации
BearerAuthenticated, DigestAuthenticated, HttpAuthenticated

dontSeeHttpHeader
// Checks over the given HTTP header and (optionally) its value, asserting that are not there.

dontSeeResponseCodeIs
// Checks that response code is not equal to provided value.

dontSeeResponseContains
// Checks whether last response do not contain text (JSON, XML).

haveHttpHeader
// Sets HTTP header

seeHttpHeader
// Checks over the given HTTP header and (optionally) its value, asserting that are there

sendGET (POST, PUT, DLETE, ... REST)

REST

amOnPage, Converting $page to valid Yii 2 URL
$I->amOnPage(['site/view','page'=>'about']);
$I->amOnPage('index-test.php?site/index');
$I->amOnPage('http://localhost/index-test.php?site/index');

attachFile, Attaches a file.
$I->attachFile('input[ * `type="file"]',`  'prices.xls'); // file is stored in 'tests/_data/prices.xls'

click
$I->click('Logout');                        // simple link
$I->click('Submit');                        // button of form
$I->click('#form input[type=submit]');      // CSS button
$I->click('//form/*[ * `type=submit]');     // XPath
$I->click('Logout', '#nav');                // link in context
$I->click(['link' => 'Login']);             // using strict locator

dontSee
$I->dontSee('Login');                  // I can suppose user is already logged in
$I->dontSee('Sign Up','h1');           // I can suppose it is not a signup page
$I->dontSee('Sign Up','//body/h1');    // with XPath

seeCookie
$I->seeCookie('PHPSESSID');

seeCurrentUrlEquals
$I->seeCurrentUrlEquals('/');

seeElement
$I->seeElement('.error');
$I->seeElement('//form/input[1]');

seeInCurrentUrl
$I->seeInCurrentUrl('home');    // to match: /home/dashboard
$I->seeInCurrentUrl('/users/'); // to match: /users/1

Yii2

cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Фикстуры

/* Codeception - Really basic class to store data in global array and use it in Cests/Tests */
Fixtures::add('user1', ['name' => 'davert']);
Fixtures::get('user1');

Важная составляющая тестирования. Их основная задача заключается в подготовке окружения с заранее фиксированным/известным состоянием для гарантии повторяемости процесса тестирования.

Yii предоставляет фреймворк, который позволяет легко и точно определять фикстуры и использовать их в ваших тестах.

Если вы используете Codeception для тестирования вашего кода, вам следует рассмотреть вопрос об использовании расширения yii2-codeception, которое имеет встроенную поддержку загрузки фикстур и доступа к ним.

cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Фикстуры

namespace app\tests\unit\models;

use yii\codeception\DbTestCase;
use app\tests\fixtures\UserProfileFixture;

class UserProfileTest extends DbTestCase
{
    public function fixtures()
    {
        return [
            'profiles' => UserProfileFixture::className(),
        ];
    }

    // ...методы тестирования...
}

Фикстуры перечисленные в методе fixtures() будут автоматически загружены перед выполнением каждого метода тестирования тест-кейса и выгружены после завершения каждого метода тестирования.

$row = $this->profiles['user1'];        // вернет строку данных для псевдонима 'user1'
$profile = $this->profiles('user1');    // вернет модель UserProfile
foreach ($this->profiles as $row) ...   // обход данных фикстуры в цикле

Иногда очень сложно протестировать испытываемую систему, потому что она зависит от других компонентов, которые нельзя использовать в тестовом окружении.

 

Когда мы пишем тест в котором не можем (или не хотим) использовать реальные зависимости, мы можем заменить их тестовыми макетами. Тестовые макеты не должны вести себя в точности как реальные компоненты; они должны предоставлять тот же API, который предоставляет и реальный компонент, так, чтобы тестируемая система думала что они реальны!

cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Заглушки (Stubs)

// Проверяем, что метод вызван как минимум один раз
// Первый параметр - Класс
// Второй параметр - Методы и свойства класса
// 
$user = Stub::make(
    'User',
    [
        'getName' => Stub::atLeastOnce(
                        function() { return 'Davert';}
                    ),
        'someMethod' => function() {}
    ]
);

$user->getName();
$user->getName();

Stub-объекты, или объекты-заглушки, — это мок-объекты, у которых все методы по умолчанию перекрывают реальные методы и возвращают null.

В PHPUnit в синтаксисе нет деления на моки и стабы. Все искусственные объекты для тестирования создаются с помощью getMock или getMockBuilder. 

cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Заглушки (Stubs)

// Делаем, что бы метод возвращал значения поочередно
$user = Stub::make(
    'User',
    ['getName' => Stub::consecutive('david', 'emma', 'sam', 'amy')]
);

$user->getName(); //david
$user->getName(); //emma
$user->getName(); //sam
$user->getName(); //amy


// Можно переопределить конструктор
// Первый параметр - Класс
// Второй параметр - Параметры для конструктора
// Третий параметр - Методы и свойства объекта
Stub::construct(
    'User',
    [],
    ['save' => function () { return true; }]
);
cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Моки (mock)

Методика замены объекта тестовым макетом, который подтверждает ожидания, например, утверждение того, что метод был вызван, называется моками (mocking).

Можно использовать мок объект "как наблюдателя, который используется, для подтверждения косвенных выводов данных тестируемой системы в то время как она выполняется. Обычно, моки включают в себя функциональность заглушек, в том, что они должны возвращать значения в тестируемую систему, если она ещё не провалила тест. Но упор делается на косвенных выводах информации. Поэтому мок объекты это больше чем заглушка плюс утверждение состояния - это фундаментально другой метод тестирования".

cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Результат покрытия тестами

// Нужен xDebug!

// codeception.yml
coverage:
    enabled: true

codeception run unit --coverage-html

// Отчет:
tests/codeception/_output/coderage/index.html
cxvxcvxcv
cxvxcvxcvcxvxcvxcv

Результат покрытия тестами

Применение

Проверка работоспособности

 

Доработка проекта

 

Рефакторинг
    - Берем код
    - Пишем тесты
    - Рефакторим
    - Проверяем тесты
    - Рефакторим снова

    - Сломалось - чиним код либо тесты

 

Нивелирование человеческого фактора

 

CI
    - Если работает на тестовом, то деплоим
    - Если не работает - не принимаем pull

Миграция сайта
 

Регресс
    - При добавлении функций
    - При обновлении фреймворка
    - При обновлении библиотек

Всем спасибо,

я побег!

Testing

By James Jason

Testing

  • 673