Skip to content

Instantly share code, notes, and snippets.

@codedokode
Last active April 25, 2024 20:24
Show Gist options
  • Save codedokode/a455bde7d0748c0a351a to your computer and use it in GitHub Desktop.
Save codedokode/a455bde7d0748c0a351a to your computer and use it in GitHub Desktop.
Автоматизированное тестирование

Автоматизированное тестирование

Обычно после написания кода его проверяют. Если речь о какой-то функции, то можно написать простой скрипт, который будет вызывать ее с разными аргументами, и смотреть, что она вернет. Если вы сделали сайт или приложение, то вы открываете его, жмете ссылки и кнопки, проверяете что все отображается верно. Это называется ручное тестирование или QA (Quality Assurance — контроль качества) — человек проверяет работу программы. Если мы попробуем автоматизировать этот процесс, и написать программу, которая проверяет правильность другой программы, то это называется автоматизированное тестирование.

Главный плюс автоматических тестов — то, что они выполняются намного быстрее, чем ручное тестирование, и вам не надо тратить на это свое время (или время тестировщика). Это позволяет запускать их хоть после каждого изменения в коде.

Также, тесты позволяют «защитить» написанный код. Если кто-то в команде (или вы сами) нечаянно «сломал» ваш код, тесты это обнаружат и укажут, что именно перестало работать. Поэтому править код становится комфортнее и спокойнее — не надо бояться, что в ходе правки вы сломали какой-то функционал и не заметили. Тестирование особенно полезно при разработке сложных приложений в большой команде.

Ниже будет краткий обзор разных подходов к тестированию, а в конце практические задания на написание тестов.

Что можно тестировать

Тестировать программу можно на разных уровнях: код (юнит-тесты и интеграционное тестирование), API (если оно есть) и GUI (интерфейс пользователя). Разные виды тестов лучше подходят в разных ситуациях.

Виды тестов

Код тестируется на разных уровнях:

Юнит-тесты (вики) — это тестирование одного элемента кода (например, отдельная функция или класс в случае ООП-кода) в изоляции от остальной части программы. Это значит, что если код обращается к каким-то другим классам, то вместо них подсовываются классы-заглушки (моки и стабы). Если код обращается к файлам, базе данных, по сети, то это все тоже заменяется на заглушки, возвращающие заранее подготовленные данные. Это делается потому, что в юнит-тестировании мы тестируем именно одну функцию, а не правильность работы базы данных, жесткого диска или удаленного сервера.

Стабы — это классы-заглушки, которые вместо выполнения действия возвращают какие-то данные. Например, стаб класса работы с базой данных может вместо реального обращения к базе данных возвращать, что запрос успешно выполнен. А при попытке прочитать что-то из нее возвращает готовый массив с данными.

Моки — это классы-заглушки, которые используются чтобы проверить, что определенная функция была вызвана с определенными аргументами.

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

Интеграционные тесты тестируют какой-то компонент системы, обычно состоящий из многих модулей (классов или функций). Например, для блога мы можем тестировать, что при вызове функции сохранения поста в базе данных появляется этот пост, у него верно проставляются теги, число комментариев равно нулю. А при добавлении комментария оно увеличивается на один. Заодно, можно протестировать, например что пост с незаполненным названием не сохраняется.

Для того, чтобы избежать ошибок и не зависеть от внешних условий, интеграционное тестирование производится в контролируемом окружении. Например, перед каждым тестом создается временная база данных с заранее подготовленными записями (к примеру, пользователями блога), очищаются папки для хранения временных файлов, а вместо запросов к внешним сервисам используется заглушка, возвращающая заранее подготовленные ответы. Если это не сделать, то мы можем получать ошибки например из-за того, что пытаемся вставить в базу пользователя с уже используемым email, из-за отстутвия какого-нибудь файла или из-за ошибки внешнего сервиса. Тесты будут чаще падать, а мы будем тратить время на выяснение причин. Также, тестовый сайт часто разворачивается на отдельном сервере или виртуальном хосте.

Ради ускорения выполнения тестов, обычно используют базу данных, храняющую данные в памяти, а не на диске (MySQL и sqlite умеют это).

Если проводить аналогии, например с тестированием авиадвигателя, то юнит-тесты - это тестирование отдельных деталей, клапанов, заслонок, а интеграционное тестирование — это запуск собранного двигателя на стенде.

Тестировать можно не любой код. Если в вашем коде жестко прописаны параметры соединения с базой данных или пути к папкам без возможности их поменять, вы вряд ли сможете использовать для тестов временную БД. То же самое, если классы в вашем коде сильно связаны и вы не используете dependency injection, если используются глобальные переменные или везде статические методы. В общем, пишите качественно.

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

API (wiki) — это набор функций, которые можно вызывать, чтобы получить какие-то данные. Например, у яндекс-карт есть АПИ геокодера. Отправив к нему запрос с географическим адресом, вы можете получить координаты точки (и наоборот), а у Центробанка есть API, которое возвращает официальный курс валют в заданный день.

Если у вашего приложения есть API, то можно тестировать его, посылая заранее подготовленные запросы и сравнивая пришедший ответ с ожидаемым. Тестировать приложение через API удобно и несложно.

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

GUI — это графический интерфейс, то есть то, что пользователь видит на экране. Это, пожалуй, самая сложная для тестирования вещь, если речь идет например о проверке работы сайта, то мы должны как-то эмулировать работу браузера, который довольно сложно устроен, анализировать информацию, которая выводится на странице. Но этот вид тестирования очень важен, так как он взаимодействует с приложением так же, как и пользователь.

GUI тесты еще называют End-to-End (E2E) или приемочные (aceptance) тесты.

Обычно GUI тестируется с помощью скриптов, которые описывают последовательность действий и проверяют ожидаемый результат. Например, скрипт тестирования формы регистрации может работать по такому алгоритму:

зайти на страницу http://example.com/register
ввести в поле «Email» значение [email protected]
ввести в поле «Пароль» значение 123456
ввести в поле «Повторите пароль» значение 123456
нажать кнопку «Зарегистрироваться»
дождаться загрузки страницы с таймаутом 5 секунд
убедиться, что на странице выводится текст «Вы зарегистрированы на сайте»

Для тестирования веб-приложения (сайта) необходимо имитировать работу браузера. Для этого есть разные подходы. Есть простые инструменты, которые лишь умеют отправлять HTTP-запросы к серверу и анализировать полученный HTML-код, и более продвинутые, которые либо используют настоящий браузерный движок (вроде PhantomJS) в «headless» режиме (то есть без вывода окошка со страницей на экран), а самые продвинутые предоставляют драйверы, с помощью которых можно контролировать реальный браузер (Selenium).

Простые HTML-браузеры хороши тем, что работают гораздо быстрее, но они не интерпретируют CSS- и JS-код и не могут проверить, к примеру, видимость кнопки или работу скриптов. PhantomJS это умеет. Selenium позволяет получить наиболее полноценную имитацию действий пользователя, в том числе например тестирование в определенной версии браузера или использование флеш-плагина, но сложен в настройке: требуется где-то запускать эти браузеры, надо поднимать сеть виртуальных машин с нужными операционными системами и тесты на нем медленнее работают.

PhantomJS и Selenium умеют делать скриншот страницы, который можно будет посмотреть при неудачном выполнении теста.

Как пишутся тесты

Составление тестов начинается со сбора списка требований к тестируемому коду. Затем для каждого требования пишется тестовый сценарий, который проверяет, что оно выполняется. Сценарии бывают позитивными — когда мы передаем корректные данные и ждем успешный результат, и негативными — когда мы передаем неправильные данные и ждем сообщение об ошибке.

Вот пример сбора требований. Допустим, что у нас есть функция truncate, которая обрезает строку, если она длиннее N символов. Каким требованиям она должна соответствовать?

  • если передать ей строку hello из 5 символов и N = 10, функция должна вернуть строку без изменений
  • если передать ей строку hello world из 11 символов и N = 5, то функция должна обрезать строку до hello…

Вот более сложный пример. Допустим, у нас есть система бронирования номеров в отеле, и мы хотим написать интеграционные тесты для класса ReservationService, который отвечает за создание броней. Тут список требований будет другой:

  • если передать корректные дату заезда, дату выезда, количество человек и в наличии есть свободные номера, то должна создаться бронь
  • если дата выезда раньше или совпадает с датой заезда, должна вернуться ошибка
  • если количество человек меньше или равно нулю, должна вернуться ошибка
  • если свободных номеров на выбранные даты нет, должна вернуться ошибка

На каждое требование мы пишем отдельный тест - это позволит при ошибке понять, что именно сломалось. Тесты обычно пишут в стиле Arrange, Act, Assert. Мы сначала подготавливаем и настраиваем нужные компоненты (Arrange), выполняем действие (Act) и проверяем результат (Assert).

Тесты обычно пишут не с нуля, а с использованием фреймфорка. Для PHP есть 2 популярных фреймворка - это PhpUnit и Codeception. Вот пример PhpUnit-тестов для описанной выше функции truncate:

class TruncateTest extends \PHPUnit\Framework\TestCase
{
    public function testShortStringRemainsAsIs()
    {
        // Act: вызываем функцию
        $result = truncate("hello", 10);
        // Assert: проверяем, что возвращенный результат совпадает с ожидаемым
        $this->assertEquals("hello", $result);
    }

    public function testLongStringIsTruncated()
    {
        $result = truncate("hello world", 5);
        $this->assertEquals("hello…", $result);
    }
}

Метод assertEquals() из PhpUnit проверяет, что фактический результат совпадает с ожидаемым, и выдает ошибку, если это не так. В PhpUnit много assert-функций на все случаи жизни.

Какими должны быть тесты

  • маленькими - идеальный тест укладывается в 5-15 строк и проверяет только одно требование. Он должен быть легко читаемым, чтобы с первого взгляда был очевиден сценарий, по которому происходит тестирование.
  • повторяемым - тест должен выдавать одинаковый результат при каждом запуске. Стоит избегать использования случайных величин, так как иначе может получиться «нестабильный» (flaky) тест, который то работает, то нет, и каждый раз надо тратить время, и искать причину.
  • тест не должен использовать тот же алгоритм, что и проверяемый код, так как можно сделать одинаковую ошибку и в коде и в тесте, и не заметить её. Например, если мы тестируем функцию решения уравнения, то проверять ее можно подстановкой решения обратно в уравнение.

Тесты должны выполняться в контролируемом окружении.

Надо тестировать и позитивные, и негативные сценарии. К примеру, при тестировании формы регистрации надо проверить не только как она работает при вводе правильных данных, но и как она работает с неправильными данными (должна выдавать сообщение об ошибке).

Тесты должно быть легко запустить, в идеале одной командой. Если для его запуска надо выполнить много действий, то людям будет лень это делать. В компаниях обычно настраивают CI сервер, который сам выкачивает обновления из репозитория, запускает тесты, и рассылает разработчикам сообщения при ошибках.

Каким должен быть код

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

Тестируем без фанатизма

При тестировании не стоит впадать в крайности. Например, нельзя говорить, что «100% кода должно быть покрыто юнит-тестами». Тесты должны прежде всего повышать качество кода, и требуют времени на их написание, отладку, поддержку. Если эти затраты больше, чем приносимая от них выгода, возможно они не требуются.

Например, если вы делаете небольшой сайт, который потом не надо поддерживать, то наверно проще просмотреть его глазами, и сдать, чем тратить время на тесты. А в некоторых случаях высокая надежность не требуется. Если местный городской сайт не работает некоторое время, ничего страшного не случится.

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

Также, есть подход, когда сначала пишутся тесты (которыми задаются требования к коду), а только потом сам код. Это называется TDD. Есть также его последователь BDD, где сценарии пишутся на странном языке Gherkin и напоминают обычный текст.

Дополнительные способы тестирования и повышения надежности кода

Использование assertions и тайп-хинтов. assert() (мануал) — это функция, которая выдает ошибку если условие в скобках не выполняется. Например, если вы написали функцию, и она принимает только числа от 0 до 5, логично в начале поставить assert для проверки этого:

function doSomething(int $a) 
{
    assert($a >= 0 && $a <= 5);
    ...

С одной стороны, вы сразу же обнаружите ошибку если передаете в нее что-то не то, с другой стороны, это документирует код и делает его более понятным.

Type hint — это конструкция, которая указывает, какой тип должен иметь аргумент или результат функции (а в PHP ≥ 7.4 можно еще указывать тип для свойств класса). Если нарушить это требование, то произойдет ошибка. Например:

function doSomeWork(SomeClass $a, string $b): int 

Статический анализ кода. Специальная программа проверяет исходники, не запуская код, с целью найти опечатки и неправильные куски кода. Такая программа хорошо ищет ошибки ситаксиса: $x = ($x + 1; (пропущена скобка), опечатки вроде if ($x = 1) (используется = вместо == в if), опечатки в именах переменных, функций и классов, обращение к несуществующей переменной.

Для PHP есть анализаторы PhpStan, Phan и Psalm.

Fuzz testing — это тестирование на основе случайно сгенерированных данных. Оно может применяться, например, для поиска уязвимостей или проверки работы кода при подаче на вход неожиданных значений.

Smoke testing — это тесты, которые проверяют общую работоспособность программы. Например, для сайта скрипт тестирования может обходить страницы из подготовленного списка и проверять, что они вообще загружаются и содержат хоть какой-то текст (например, название сайта). Также, можно жать на все кнопки на странице и проверять, что при этом не возникает яваскрипт-ошибок.

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

Вот ссылка про нагрузочное тестирование в Яндексе: http://habrahabr.ru/company/yandex/blog/202020/

Регрессионное тестирование — это тестирование, что ранее обнаруженная ошибка больше не встречается. Если вы нашел какой-то баг, вы пишете тест на него и больше он не останется незамеченным.

Тестирование на основе скриншотов — статья от Яндекса http://habrahabr.ru/company/yandex/blog/200968/

Инструменты

Тесты гораздо удобнее писать на основе готовых библиотек и фреймворков, чем с нуля. Некоторые из них интегрируются с IDE и позволяют запускать тесты нажатием кнопки. Вот популярные инструменты для тестирования веб-приложений на PHP/JS.

Тестирование PHP кода

PhpUnit

Сайт (англ.), перевод документации, в гугле много статей по нему. Несмотря на название, он годится не только для unit-тестов, но и для интеграционных, а так же браузерных через Selenium. Он также представляет функции для генерации моков и стабов из существующих классов.

Сам phpUnit распространяется в виде одного файла phpunit.phar, и вы запускаете его командой вроде

php phpunit.phar ....

Чтобы увидеть список всех доступных опций, можно набрать

php phpunit.phar --help

(если вы не работали с командной строкой и плохо понимаете, о чем речь, прочтите мой урок по командной строке).

Тесты для phpunit хранятся в файлах (их может быть много, и при желании их можно раскладывать по папкам), каждый файл содержит 1 класс, унаследованный от встроенного в phpUnit класса PHPUnit\Framework\TestCase. А этот класс может содержать 1 или больше методов с конкретными тестовыми примерами.

Обычно название класса теста соответствует названию того класса или функции, которые он проверяет. Например, для тестирования класса src/Hotel/RoomManager.php логично создать тест tests/Hotel/RoomManagerTest.php.

Каждый метод, имя которого начинается с test…, будет выполнен. Также, вы можете добавить методы setUp и tearDown, которые будут вызываться до и после каждого теста. Выше был приведен пример теста на основе PhpUnit.

Запустить тест можно командой

php phpunit.phar TruncateTest.php

PhpUnit выполнит его и выведет результаты. Если у вас много тестов, можно указать только имя папки и phpUnit сам найдет все файлы в ней, имена которых заканчиваются на …Test.php и выполнит их.

Если вы тестируете существующий код, то часто что-то требуется сделать до выполнения тестов, например, подключить автозагрузчик или задать значение какой-то переменной. Для этого можо создать файл, например, bootstrap.php. Если вы используете композер, то подключение автозагрузки делается одной строчкой:

require_once '../vendor/autoload.php';

Если нет, то потребуется чуть больше кода. После этого, мы можем указать phpunit подключить этот файл с помощью опции --bootstrap:

php phpunit.phar --bootstrap bootstrap.php TruncateTest.php

Обычно для тестов создают папку с названием tests в корне проекта.

Настройки для phpunit также можно задать не опциями, а в XML-файле phpunit.xml (что такое XML? wiki). Формат файла описан в мануале. Это удобнее,так как их в этом случае не надо печатать в командной строке. Вот как будет выглядеть файл для примера выше:

<phpunit bootstrap="bootstrap.php">
    <testsuites>
        <testsuite name="count">
            <file>FunctionCountTest.php</file>
        </testsuite>
  </testsuites>  
</phpunit>

Соответственно, для запуска тестов достаточно набрать

php phpunit.phar

Подвох: phpunit при выполнении тестов перехватывает и скрывает все, что выводится с помощью echo и аналогичных функций. Если вам надо что-то вывести для отладки, используй конструкцию вроде var_dump($x); die();.

Ссылки про phpUnit:

phpunit интегрируется в IDE:

  • Eclipse PDT через плагин MakeGood или PTI (англ.)
  • Netbeans (я подозреваю, что можно обойтись без установки PEAR, достаточно указать путь к phpunit.phar)
  • PhpStorm, мануал (англ.) — достаточно нажать в настройках кнопку скачивания phpunit.phar

В этом случае вы можете запускать и просматривать результаты тестов прямо в IDE. Но учиться лучше с использованием командной строки.

Codeception

Cайт codeception. Этот фреймворк заточен на написание API и GUI тестов (хотя он включает в себя phpunit и может выполнять его тесты, но удобнее их хранить отдельно). Он может работать как с примитивным html-браузером на основе Symfony BrowserKit (не интерпретирующим CSS и JS), так и с PhantomJS и Selenium. Также, он может использоваться для «функционального» тестирования, то есть вызова контроллеров фреймворка напрямую (без запуска веб-сервера и использования HTTP). Для этого у него есть плагины к разным популярным фреймворкам. Причем синтаксис скриптов для всех этих случаев примерно одинаков.

Codeception, как и phpUnit, распространяется в виде одного файла codecept.phar. После скачивания можно запустить команду

php codecept.phar bootstrap

Чтобы он создал нужную структуру папок для тестов.

Вот как может выглядеть скрипт проверки формы регистрации. Код на нем похож на текст на английском языке и видимо вдохновлен behat (который вдохновлен рубиевским cucumber).

// Создаем объект-тестировщик
$I = new AcceptanceTester($scenario);

// Задаем название теста (оно отображается в отчете)
$I->wantTo('Test that registration works');

// Переходим на страницу
$I->amOnPage('/register');

// Заполняем форму
$I->fillField('email', '[email protected]');
$I->fillField('password', '123456');
$I->fillField('password-confirm', '123456');

// Отправляем нажатием кнопки
$I->click('Register');

// Проверяем результат
$I->see('You have registered');

// Проверяем что запись появилась в БД
$I->seeInDatabase('users', ['email' => '[email protected]']);

Команда, чтобы запустить этот скрипт, есть на сайте.

Как я уже писал выше, этот скрипт может фактически работать с использованием разных уровней эмуляции браузера (первый способ самый быстрый и простой, последний самый точный и мощный, но медленный):

  • вызывать напрямую контроллер, если ваше приложение использует поддерживаемый фреймворк (поддерживаются ZF1-2, Yii1-2, Symfony2, Silex. Slim не поддерживается, никто не желает написать плагин и выложить в опенсорс?)
  • используя PhpBrowser, то есть скачивание HTML-страниц через HTTP
  • контролируя PhantomJS через WebDriver
  • контролируя реальный браузер через Selenium

В codeception есть много удобных вспомогательных функций для заполнения форм, отправки файлов, поиска элементов на странице с помощью XPath и CSS селекторов, работы с куками, заголовками, аякс-запросами, всплывающими окнами, ифреймами, снятия скриншотов, навигации. Эти функции разбиты на модули (например, модуль для тестирования АПИ, модуль для работы с базой данных), и вы можете писать свои или расширять существующие модули через наследование.

Ссылки:

Behat, phpspec

Библиотеки для написания тестов в стиле BDD: http://docs.behat.org/en/v2.5/ (англ), http://www.phpspec.net/

Тестирование JS кода

Mocha

Mocha, гитхаб (читается «мока», кто бы поверил) — фреймворк для тестирования яваскрипт-кода, например приложений. Он использует подход BDD.

Zombie.js

Это фреймворк для написания тестов, имитирующий браузер (поддерживается CSS/JS) за счет JSDOM, то есть реализации DOM под Node.JS. Внутри он также использует вышеупомянутый Mocha. Его можно использовать для тестирования верстки и яваскрипта. Ссылка: http://zombie.labnotes.org/ http://zombie.js.org/

Вот пример теста, тестирующий позитивный сценарий успешной регистрации на некоем воображаемом сайте (код я не проверял, так что могут быть небольшие ошибки). Этот код запускается под Node.JS:

const Browser = require('zombie');

// В тесте мы будем отправлять запросы на адрес http://example.com/signup
// однако эти запросы будут обслуживаться установленным локально 
// сервером на порту 3000 с адресом localhost:3000
Browser.localhost('example.com', 3000);

// describe описывает сценарий теста, 
// причем блоки describe могут вкладываться друг в друга
describe('Пользователь заходит на страницу регистрации', function() {

  // создаем объект, имитирующий браузер
  const browser = new Browser();
  
  // Действие, выполняемое до теста - переход на нужную страницу
  before(function(done) {
    browser.visit('/signup', done);
  });

  describe('пользователь заполняет и отправляет форму', function() {

    before(function(done) {
      browser
        .fill('email',    '[email protected]')
        .fill('password', '123456')
        .fill('password-confirm', '123456')
        .pressButton('Зарегистрироваться', done);
    });

    // it описывает конкретное требование и код для его проверки
    it('регистрация должна быть успешной', function() {
      browser.assert.success();
    });

    it('пользователь должен увидеть приветственное сообщение', function() {
      // Уведомление должно вывестись в элементе с классом notification 
      // (используется CSS-синтаксис селекторов)
      browser.assert.text('.notification', 'Вы успешно зарегистрированы');
    });
  });
});

Jasmine

Jasmine (англ.) - это тоже фреймворк для тестирования JS-кода с уклоном в BDD (Behaviour-Driven Development). Если вы забыл, то идея BDD в том, что мы до разработки описываем требования (спецификацию) к коду в виде тестов. Он позволяет писать простые, легко читаемые тесты. Запускать тесты можно как через браузер (если вы хочете смотреть релуьтаты глазами), так и из командной строки с помощью Node.JS (если проверку надо автоматизировать). Если твои тесты используют взаимодействие с DOM (или какие-то другие компоненты браузера) то для запуска под Node.JS придется подключить упомянутый выше JSDOM. Вот пример простого теста на Jasmine, которым мы протестируем функцию вычисления квадратного корня Math.sqrt():

describe("Math.sqtr() это функция", function() {
    it("вычисляет квадратный корень", function() {
        expect(Math.sqrt(9)).toBe(3);
    });
    
    it("возвращает нуль, если попытаться найти корень из нуля", function () {
        expect(Math.sqrt(0)).toBe(0);
    });
    
    it("возвращает NaN при попытке вычислить корень из отрицательного числа", function () {
        expect(Math.sqrt(-9)).toBe(NaN);
    });
});

Если не обращать внимание на некоторую нечитаемость текста из-за смеси русского и английского, то в общем код теста действительно напоминает список требований к функции. Мы описываем конкретные тестовые случаи с помощью конструкций expect(...).toBe(...) где указываем пример выполняемого кода и ожидаемый результат.

Вот результат выполнения этих тестов в браузере, где Jasmine выводит список пройденных проверок:

Результат запуска теста Jasmine в браузере

Jasmine расширяемый и вы можете дописывать свои проверяльщики (matchers) и свой код для вывода результатов в удобном вам виде.

Дополнительные инструменты

Skipfish

Skipfish (англ.) — инструмент от Google, который может использоваться для поиска ошибок на сайте и заодно для нагрузочного тестирования. Он обходит все страницы, начиная со стартовой и перемещаясь по ссылкам, и позволяет обнаруживать битые ссылки (в том числе на картинки, CSS и JS файлы). Также, он умеет отправлять запросы со случайно сгенерированными данными и пытается искать явные XSS/SQL уязвимости. Он работает очень быстро (если конечно сайт может отвечать быстро).

Skipfish — это не совсем средство автоматического тестирования, так как результаты работы выдаются в виде html-отчета, но он может быть полезен например для поиска ошибок на существующем сайте.

Skipfish генерирует большую нагрузку на сайт и шлет очень много запросов, потому применяй его только на своих сайтах.

PhantomJS

PhantomJS — это браузерный движок (используется Webkit — тот же, что используется в Safari, Opera, Яндекс-браузере и старых версиях Хрома), которым можно управлять с помощью скриптов на яваскрипте. Это headless браузер, то есть он не выводит никаких окон (и вообще не требует наличия видеокарты и дисплея), а работает как приложение командной строки. Он кроссплатформенный и его можно запускать, например, автоматически на линукс сервере. Он умеет переходить по страницам, загружать CSS/JS (при желании и картинки), делать скриншоты, выполнять произвольный JS код в контексте страницы.

Также, для PhantomJS есть плагин ghostdriver (WebDriver), который позволяет подсоединиться к программе извне и управлять ей. Он использует протокол Selenium, и с его помощью PhantomJS можно управлять из codeception.

Тесты по идее можно писать на яваскрипте, скармливая их напрямую PhantomJS, но удобнее использовать какую-нибудь библиотеку работающую поверх него, например, Casper.js (это если вы готов писать тесты на явскрипте, если на PHP, то стоит использовать codeception + PhantomJS через WebDriver).

Для повышения скорости работы теста стоит отключить загрузку картинок, если они не требуются для теста.

Статьи по использованию PhantomJS с codeception наверно нетрудно нагуглить.

Selenium

Selenium — это проект, предоставляющий драйвера для разных браузеров, которые встраиваются в них и позволяют управлять ими. Также, Selenium содержит сервер, который позволяет управлять большим числом разных браузеров ии распределять задания между ними.

Selenium сервер написан на Яве, потому она понадобится чтобы его запустить. Поддерживаются браузеры Firefox, IE (6-11), Safari на OS X, Opera 12 (старая Опера), Chrome.

Selenium дает наиболее полноценное тестирование, так как вы можете запускать код в конкретной версии браузера (например, IE) под конкретной ОС. Но его настройка сложнее чем других инструментов, и он требует больше ресурсов. Когда выполняется тест, браузер должен быть запущен и вы не можете пользоваться компьютером, так как один лишний клик может сорвать выполнение теста. По этой причине для тестов обычно поднимают сеть вирутальных машин, в которых тесты и выполняются.

Так как настроить окружение для запуска тестов сложно, есть коммерческие сервисы (например saucelabs) которые за плату выполняют selenium-тесты на нужных браузерах и возвращают результат. Они предоставляют API с помощью которого тесты можно запускать автоматически и умеют отслеживать изменения в репозитории, тестируя код при каждом новом коммите.

Тесты в браузере содержат подвохи: например, когда вы программно нажимаете кнопку, браузеру нужно время, чтобы выполнить привязанный к ней яваскрипт, обработать изменения в DOM, перерисовать экран. Если ваш скрипт не будет дожидаться этого, а попробует сразу после нажатия кнопки проверить изменения на экране, он может их не увидеть. В некоторых статьях вы можете увидеть совет вроде «делать паузу N мс после каждого шага», но это плохие советы. Во-первых, нет гарантий, что действие выполнится за эти N мс, во-вторых, это сильно тормозит тесты. Лучше, как советует Яндекс, в таких случаях периодически проверять появление определенного элемента на странице.

Также, не так просто проверить, что элемент видим. Ведь в CSS много свойств (opacity, visisbility, display), которыми можно его скрыть, плюс он может быть помещен за пределами экрана.

Тесты в браузере могут быть хрупкими. У браузера может выскочить окно обновления, может произойти ошибка при загрузке какого-то внешнего ресурса, в общем, сложностей тут много. А кто говорил, что будет легко?

Также, Selenium содержит плагин к фаерфоксу (Selenium IDE), который позволяет записывать действия пользователя и генерировать из них тест (то есть повторять эти действия позже), но он, как я понимаю, довольно слабый и генерирует тяжелочитаемый код на своем странном языке. Гораздо лучше управлять Selenium из codeception и писать тесты на PHP.

Статьи:

Travis CI

Чтобы не запускать тесты каждый раз руками, используют CI Server вроде Teamcity, Jenkins или Hudson (статья от Яндекса: http://habrahabr.ru/company/yandex/blog/237017/ ). Но эти системы надо устанавливать и настраивать, в то время как Travis это сервис, который сам будет подсоединяться к твоему гитхаб репозиторию, брать оттуда изменения и прогонять тесты. Для open source проектов на github Travis CI бесплатен.

Ссылки

Статья с теорией и определениями разных терминов (что такое тест-кейс, какие бывают уровни тестирования, серьезность дефекта, виды тестирования ит.д.): Тестирование. Фундаментальная теория

На хабре хорошие статьи про тестирование в блогах Яндекса и Баду:

Вот примеры разных open source проектов c тестами (кстати, многие из них используют travis CI):

Задачи

Разумеется, чтобы закрепить знания, а также изучить практические особенности тестирования, нам надо решить задачки и что-нибудь потестировать. Предлагаю тестировать приложение-файлообменник, у меня их аж несколько:

вы форкаете тот, который вам больше нравится (или меньше не нравится), и мы пишем под него юнит-тесты на phpUnit и интерфейсные тесты на codeception.

Если вам не нравится файлообменник, можно потестировать что-то еще (если есть идеи небольшого приложения на PHP, требующего тестов — напишите).

Прежде чем писать тесты, надо составить план тестирования (что мы тестируем и как). Мы сначала определяем, какие возможности предоставляет приложение, а потом для каждой пишем несколько тест кейсов. Не забывайте, что надо тестировать как позитивные, так и негативные сценарии. Вот примерный план, 1 пункт = 1 тест.

  • Загрузка файла
    • Загрузить файл и убедиться что он загрузился
    • Отправить пустую форму без файла
    • Загрузить слишком большой файл
    • Загрузить файл c utf-8 символами в названии и проверить что все отображается корректно
  • Страница файла
    • Загрузить файл и убедиться что он доступен для скачивания. Скачиваемый файл совпадает с загруженным
    • Убедиться что выводится правильная информация о файле
  • Медиаданные
    • Загрузить картинку и убедиться что превьюшка выводится и она совпадает с картинкой
    • Загрузить картинку неподдерживаемого типа вроде tif/bmp
    • Загрузить аудиофайл и убедиться что доступен плеер
    • Загрузить видеофайл и увидеть плеер
  • Комментарии
    • Отправить комментарий
    • Отправить пустой комментарий
    • Отправить комментарий с HTML кодом
    • Отправить комментарий-ответ
  • Последние файлы
    • Загрузить 3 файла и проверить что они отображаются в правильном порядке с правильными ссылками

Также, стоит написать тест на codeception который обходит сайт и проверяет отстутвие битых ссылок.

В качестве базы для тестов стоит использовать in-memory mysql базу.

Если у вас есть какие-то дополнения к плану, можно их внести.

Что тестировать юнит-тестами? Ими можно тестировать например функцию валидации (комментария к примеру), а также функции вроде форматирования размера файла (которые выводят его в виде «12Мб»).

Также, надо настроить интеграцию с Travis CI, чтобы тесты выполнялись на нем.

Связаться с автором

[email protected]

@dankodima1
Copy link

dankodima1 commented Aug 15, 2021

Вообще-то тесты не делятся на UNIT-тесты и интеграционные. Интеграционные тесты используются при написании Unit-тестов. Unit - тесты это инструмент. В статье имелось в виду деление тестов на Модульные и Интеграционные.

@fey
Copy link

fey commented Aug 15, 2021

Модульные = юнит тесты.
И градация зависит от система (т.е. у каждого своя)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment