Часть 2: Тестирование простого приложения (Тестирование ПО)
Оглавление
Первое приложение
Программа считывает три целочисленных значения из консоли. Эти значения интерпретируются как длины сторон треугольника. Программа выводит сообщение о том, каким является данный треугольник - разносторонним, равнобедренным или равносторонним.
Напишем, функцию, которая принимает на вход три стороны треугольника, которые заданы целыми числами и возвращает тип треугольника. Сохраним написанный код в файле triangle.php.
/**
* Не треугольник
*/
define('TRIANGLE_BAD', 0);
/**
* Равносторонний треугольник
*/
define('TRIANGLE_EQUILATERAL', 1);
/**
* Прямоугольный треугольник
*/
define('TRIANGLE_RIGHT', 2);
/**
* Равнобедренный треугольник
*/
define('TRIANGLE_ISOSCELES', 3);
/**
* Разносторонний треугольник
*/
define('TRIANGLE_SIDED', 4);
/**
* По длинам сторон $a, $b и $c возвращает тип треугольника.
* Если стороны не являются целочисленными, то выбрасывает исключение.
*
* @param $a
* @param $b
* @param $c
* @return int
* @throws Exception
*/
function triangle_type($a, $b, $c)
{
// Вполне ожидаемо,
// что нецелочисленные значения должны приводить к исключительной ситуации.
if (!is_int($a) or !is_int($b) or !is_int($c))
{
throw new \Exception('Invalid triangle definition');
}
$max = null;
$min1 = null;
$min2 = null;
if (($a+$b)>$c and ($a+$c)>$b and ($b+$c)>$a)
{
if (($a>$b) and ($a>$c))
{
$max = $a;
$min1 = $b;
$min2 = $c;
}
else if (($b>$c) and ($b>$a))
{
$max = $b;
$min1 = $a;
$min2 = $c;
}
else
{
$max = $c;
$min1 = $a;
$min2 = $b;
}
if (pow($max, 2) == pow($min1, 2) + pow($min2, 2))
{
return TRIANGLE_RIGHT;
}
else if (($max==$min1) and ($max==$min2))
{
return TRIANGLE_EQUILATERAL;
}
else if (($max==$min1) or ($max==$min2) or ($min1==$min2))
{
return TRIANGLE_ISOSCELES;
}
else
{
return TRIANGLE_SIDED;
}
}
else
{
return TRIANGLE_BAD;
}
}
Функция достаточно тривиальна, поэтому мы не будем останавливаться на ее реализации. Нас будет интересовать, как найти в ней ошибки.
Для начала потребуется реализовать механизм, который позволит вводить данные с консоли и получать результат. Сохраним следующий код в файле main.php. Чуть позже вы поймете, почему мы используем разные файлы для самой функции и для кода, который обрабатывает пользовательский ввод.
// здесь мы подключим ранее написанную функцию для определения типа треугольника
require __DIR__ . DIRECTORY_SEPARATOR . 'triangle.php';
function main()
{
// проинициализируем переменные
$a = $b = $c = 0;
// получим длины сторон со стандартного ввода
$num = fscanf(STDIN, "%d %d %d\n", $a, $b, $c);
// если мы смогли считать длины трех сторон,
// то вызовем нашу функцию и покажем результат
if ($num == 3)
{
switch (triangle_type($a, $b, $c))
{
case TRIANGLE_BAD:
echo "Это не треугольник\n";
break;
case TRIANGLE_EQUILATERAL:
echo "Это равносторонний треугольник\n";
break;
case TRIANGLE_ISOSCELES:
echo "Это равнобедренный треугольник\n";
break;
case TRIANGLE_RIGHT:
echo "Это прямоугольный треугольник\n";
break;
case TRIANGLE_SIDED:
echo "Это разносторонний треугольник\n";
break;
}
}
}
main();
Код также достаточно тривиален. Теперь мы можем запустить полученное приложение (да, это именно приложение - последовательность инструкций, определяющих процедуру решения конкретной задачи компьютером).
Откроем терминал, перейдем в каталог, с проектом и выполним следующую команду (для того, чтобы все сработало у вас должен быть установлен интерпретатор php в системе).
$ php main.php
Программа будет ожидать ввод трех чисел, разделенных пробелами.
И вот что мы можем увидеть на экране.
Поэкспериментируйте немного с программой вводя разные наборы чисел.
А теперь рассмотрим эту программу с точки зрения разработчика, которому досталось ее тестировать. Какие наборы тестов он должен разработать, чтобы отыскать все возможные баги? Прежде чем читать дальше подумайте и попробуйте посчитать то количество, которое придумали вы.
Итак. Ниже приведен набор тестовых сценариев, которые должны быть написаны для нашей функции.
- тест для проверки действительно неравностороннего треугольника (наборы [1, 2, 3], [2, 5, 10] треугольниками не являются).
- проверка на действительно равносторонний треугольник
- проверка на равнобедренный треугольник (наборы вида [2, 2, 4] треугольником не являются)
- как минимум три теста для проверки равнобедренного треугольника, которые представляют собой перестановки одного и того же набора чисел ([3, 3, 4], [3, 4, 3], [4, 3, 3])
- тест на нулевую длину одной из сторон
- тест на сторону, имеющую длину меньше нуля
- проверка набора чисел, в котором сумма длин двух сторон равна третьей
- тест перестановок для троек чисел из теста 7
- проверка набора чисел, в котором сумма длин двух сторон меньше третьей ([12, 15, 30])
- тест перестановок для троек чисел из теста 9
- проверка на нулевую длину всех трех сторон
- проверка на передачу нецелочисленных значений
- проверка на передачу неполного набора значений
- проверка не только входных данных, но и ожидаемого выходного значения в каждом из тестов 1-13
Если вы не смогли назвать все кейсы, то не пугайтесь. Среднее число тестов, которые называли в разное время опытные разработчики составило 7,8.
Конечно нет никаких гарантий того, что набор тестов, удовлетворяющих перечисленным условиям, обнаружит все возможные ошибки. Но поскольку случаи 1-13 представляют ошибки, реально встречающиеся в различных версиях данной программы, адекватное тестирование должно обнаружить хотя бы их.
Это упражнение должно было продемонстрировать вам, что тестирование простых программ наподобие вышеприведенной является отнюдь не тривиальной задачей. А теперь попытайтесь представить себе, насколько трудоемким окажется тестирование, скажем, бухгалтерской программы крупного предприятия, компилятора или же системы управления воздушным движением, объем кода которых может достигать сотен тысяч строк. Еще большие трудности возникают с приложениями, которые написаны с использованием объектно-ориентированных языков (куда входит и php) и подходов. В частности, тесты для подобных приложений должны выявлять ошибки с созданием экземпляров объектов и взаимодействия между ними.
Однако, какой бы устрашающей ни казалась задача, адекватное (достаточно полное) тестирование программ является ключевой и, как вы убедитесь далее, вполне реализуемой частью процесса разработки программного обеспечения.
Тестируем
Конечно же самым простым решением будет просто закодировать все тестовые случаи для нашего проекта и написать нечто вроде следующего кода (файл triangle_test_simple.php).
require __DIR__ . DIRECTORY_SEPARATOR . 'triangle.php';
function testForIsoscelesTriangle()
{
echo "Test for [3, 4, 4]: ";
if (triangle_type(3, 4, 4) == TRIANGLE_ISOSCELES) {
echo "ok\n";
} else {
echo "fail\n";
}
}
function main()
{
testForIsoscelesTriangle();
}
main();
И такое часто практикуется. Особенно в среде разработчиков на C\C++. На каждый логически связанный набор тестовых случаев создается свой файл. Который содержит множество функций обрабатывающих по одному сценарию каждая.
В этом нет ничего плохого. Единственный минус такого подхода - отсутствие готового инструментария, который реализует все необходимые операции по обслуживанию и запуску тестов. Весь инструментарий приходится для каждого проекта реализовывать заново. Либо изготавливать свою собственную обвязку, которая будет кочевать из проекта в проект.
Один из вариантов создания инструмента для работы с подобными тестами вы можете увидеть в файле triangle_test.php. Запустите его и увидите на экране подробный лог тестирования проекта.
Литература
- “Искусство тестирования программ” Гленфорд Майерс, Том Баджетт, Кори Сандлер, ISBN: 978-5-8459-1974-8
- “PHP 7” Дмитрий Котеров, Игорь Симдянов, ISBN 978-5-9775-3725-4
Исходные тексты программ
Оглавление
Часть 1: краткая историческая справка (Тестирование ПО)
PHP: дело о загадачном пробеле
Или история о том, как побились картинки.
Однажды от пользователей пришла жалоба, что новые картинки, которые они на проект грузят не отображаются. Только если полностью обновить страницу.
Проверил данные в кеше - картинки были. Все как положено - пережатые и с водяными знаками. После ряда танце с бубном удалось выяснить, что скрипт, который картинку генерирует пока ее в кеше нет отдает пробел в каждой картинке - на скриншоте это хорошо выдно. Идет пробел, а после стандартный заголовок jpeg.
Чтож. Очень вероятно, что какой-то вредный скрипт оказался с пробелом. Мы же помним вро стандартные открывающиеся и закрывающиеся теги php, которые принесли много головной боли.
Но как среди тысяч строк кода выяснить, где начинается вывод злобного пробела?
- отключить вывод самой картинки и убедится, что пробел все еще выводится.
- отключить буферизацию вывода на стороне сервера - убедится, что пробел есть
- в самом конце скрипта поставить setcookie или header - в логах пояится сообщение вида “ Warning : Cannot modify header information - headers already sent by (output started at file.php:XXX)”. Где file.php - это соответсвенно файл, а XXX - номер строки, где начался вывод.
Чтобы отключить буферизацию нужно прописать в конфигах php.ini
output_buffering=0
Или в конфигах апача или htaccess
php_flag "output_buffering" Off
OpenWRT: блокировка рекламы
Есть отличная прошивка на базе всеми любимого линупса для всякой разной техники вроде роутеров и микрокомпьютеров - это OpenWRT.
А поскольку эта штука работает в качестве локального днс-сервера, то грех не научить ее блокировать на корню всякие даблклик.нет и другое непотребство.
Рассматривать будем ситуацию при которой у нас установлена дефолтная конфигурация c dnsmasq.
Сначала нам потребуется поставить пакет dnsmasq-full взамен стандартного (он чуть больше по объему и тянет больше зависимостей, и предоставляет больше возможностей по настройке).
# opkg update
# opkg remove dnsmasq
# opkg install dnsmasq-full
# /etc/init.d/dnsmasq restart
Посмотрим на файл /tmp/etc/dnsmasq.conf. Нас будут интересовать следующие два параметра.
addn-hosts=/tmp/hosts
conf-dir=/tmp/dnsmasq.d
Буквально это значит, что дополнительные файлы в формате /etc/hosts хранятся в /tmp/hosts, а дополнительные конфиги - в /tmp/dnsmasq.d.
Теперь нам потребуется скрипт, который будет скачивать листы. Поместим его в /root/bin/adblock.sh
#!/bin/sh
echo "download adblock rules"
if [! -d /tmp/dnsmasq.d]; then
mkdir /tmp/dnsmasq.d
fi
if [! -d /tmp/hosts]; then
mkdir /tmp/hosts
fi
touch /tmp/dnsmasq.d/adblock.conf
wget -O /tmp/dnsmasq.d/adblock.conf "http://pgl.yoyo.org/adservers/serverlist.php?hostformat=dnsmasq&showintro=0&mimetype=plaintext"
echo "" > /tmp/hosts/adblock
wget -O /tmp/adblock https://hosts-file.net/ad_servers.txt
sed 's/^\(.*\).$/\1/' /tmp/adblock >> /tmp/hosts/adblock
wget -O /tmp/adblock https://adaway.org/hosts.txt
sed 's/^\(.*\).$/\1/' /tmp/adblock >> /tmp/hosts/adblock
wget -O /tmp/adblock http://winhelp2002.mvps.org/hosts.txt
sed 's/^\(.*\).$/\1/' /tmp/adblock >> /tmp/hosts/adblock
if [-f /tmp/adblock]; then
rm /tmp/adblock
fi
/etc/init.d/dnsmasq enabled && /etc/init.d/dnsmasq restart
Первым мы скачиваем файл с блокировками в формате dnsmasq, а последующие запросы - это блокировки в формате hosts. Соответственно раскладываем их по разным папкам и рестартим сервис.
Не забываем поставить +x на файл.
Теперь нужно добавить этот скрипт в крон дабы он обновлялся.
# crontab -e
и пишем туда что-то вроде
* */12 * * * /bin/sh /root/bin/adblock.sh
Это означает, что раз в 12 часов списки будут обновляться.
Естественно, что надо включить крон. По дефолту он выключен.
# /etc/init.d/cron enable
# /etc/init.d/cron enabled && /etc/init.d/cron start
Теперь осталось сделать так, чтобы при поднятии сетевого интерфейса скрипт сразу же обновлял правила.
А для этого нам потребуется создать файл /etc/hotplug.d/iface/50-adblock примерно следующего вида.
#!/bin/sh
[ifup = "$ACTION" -a "$DEVICE" = eth1] && {
/bin/sh /root/bin/update_adblock.sh
}
Где eth1 - это ваш wan-интерфейс.
Все. Можно перезагрузить роутер для проверки что все настроено корректно.
Рекламные домены вроде doubleclick.de должны резолвиться либо на 0.0.0.0, либо на 127.0.0.1.
$ nslookup doubleclick.de
Server: 192.168.0.1
Address: 192.168.0.1#53
Name: doubleclick.de
Address: 127.0.0.1
Чтобы точно убедиться, что все работает как надо - проверяйте что сразу после поднятия wan появились файлы /tmp/dnsmasq.d/adblock.conf и /tmp/hosts/adblock.
Учтите, что в сумме размер списков блокировки составит более 100 тысяч записей. И если роутер слабоват, то придется какой-то из списков отключать.
И да - это не панацея. Это просто хороший способ обезопасить электронные читалки, телефоны и разную мелкую технику, которая ходит в инет от баннеров и прочего добра.
Источники:
- https://wiki.openwrt.org/doc/networking/routing
- https://forum.openwrt.org/viewtopic.php?id=50407
- https://wiki.openwrt.org/doc/howto/cron
- https://wiki.openwrt.org/ru/doc/howto/dhcp.dnsmasq
- https://forum.openwrt.org/viewtopic.php?id=25304
- https://wiki.openwrt.org/doc/techref/initscripts
- https://wiki.openwrt.org/doc/techref/initscripts
- https://wiki.openwrt.org/ru/doc/uci/dhcp
JavaScript: копируем img в canvas
Существует несколько способов скопировать имеющееся на странице изображение (img) в canvas. Конечно все они сводятся к простейшему drawImage, но каждый из этих методов применим в своем контексте.
Первый и самый очевидный (а еще универсальный) - загрузить изображение при помощи объекта Image. Он работает только в случае, когда у img есть атрибут src.
var img = new Image();
var srcImg = document.getElementById('src-img');
img.onload = function() {
// создаем (или получаем) канву
var backCanvas = document.getElementById('dest-canvas');
backCanvas.width = this.width;
backCanvas.height = this.height;
var backCtx = backCanvas.getContext('2d');
// рисуем
backCtx.drawImage(this, 0,0);
};
img.setAttribute('src', srcImg.src);
Недостаток - еще один запрос в сеть.
Второй способ - менее универсален. Картинка у нас уже есть - почему ее нельзя нарисовать на канве?
var backCanvas = document.getElementById('dest-canvas');
var srcImg = document.getElementById('src-img');
backCanvas.width = srcImg.naturalWidth;
backCanvas.height = srcImg.naturalHeight;
var backCtx = backCanvas.getContext('2d');
// рисуем картинку
backCtx.drawImage(srcImg, 0,0);
И вот тут мы используем naturalWidth/naturalHeight. Зачем?
Все очень просто. Если в первом случае width и height означали размеры картинки, то во втором - это будут физические размеры элемента img на странице. И если картинка внутри него будет отмасштабирована, то мы мы скопируем только часть картинки размером width на height.
Как на иллюстрации ниже (картинка там имеет разрешение в десять раз больше нежели физические размеры img и масштабируется).
А свойства naturalWidth/naturalHeight не поддерживаются в ряде браузеров. поэтому способ универсальным назвать нельзя.