Разработка

Часть 3: Разработка через тестирование, TDD (Тестирование ПО)

Оглавление


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

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

Читать далее

Разработка

Часть 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

Программа будет ожидать ввод трех чисел, разделенных пробелами.

И вот что мы можем увидеть на экране.

null

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

А теперь рассмотрим эту программу с точки зрения разработчика, которому досталось ее тестировать. Какие наборы тестов он должен разработать, чтобы отыскать все возможные баги? Прежде чем читать дальше подумайте и попробуйте посчитать то количество, которое придумали вы.

Итак. Ниже приведен набор тестовых сценариев, которые должны быть написаны для нашей функции.

  1. тест для проверки действительно неравностороннего треугольника (наборы [1, 2, 3], [2, 5, 10] треугольниками не являются).
  2. проверка на действительно равносторонний треугольник
  3. проверка на равнобедренный треугольник (наборы вида [2, 2, 4] треугольником не являются)
  4. как минимум три теста для проверки равнобедренного треугольника, которые представляют собой перестановки одного и того же набора чисел ([3, 3, 4], [3, 4, 3], [4, 3, 3])
  5. тест на нулевую длину одной из сторон
  6. тест на сторону, имеющую длину меньше нуля
  7. проверка набора чисел, в котором сумма длин двух сторон равна третьей
  8. тест перестановок для троек чисел из теста 7
  9. проверка набора чисел, в котором сумма длин двух сторон меньше третьей ([12, 15, 30])
  10. тест перестановок для троек чисел из теста 9
  11. проверка на нулевую длину всех трех сторон
  12. проверка на передачу нецелочисленных значений
  13. проверка на передачу неполного набора значений
  14. проверка не только входных данных, но и ожидаемого выходного значения в каждом из тестов 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. Запустите его и увидите на экране подробный лог тестирования проекта.

null

Литература

Исходные тексты программ

Оглавление

Разработка

Часть 1: краткая историческая справка (Тестирование ПО)

Оглавление

Баг. Кто он?

Мы все слышали слово «баг», которым обозначается ошибка в программе приводящая к неверному результату.

Баг. Кто он?

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

Откуда пошло тестирование

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

В 1970-х годах процесс проверки программы обозначался как «доказательство правильности». И включал в себя проверку всех возможных путей исполнения кода.

По мере усложнения программ и решаемых задач тестирование становилось все сложнее и сложнее. И вот уже «доказательство правильности» становится недостаточно всеобъемлющей процедурой. Ибо проверить абсолютно все физически невозможно. Да, тогда и сегодня все еще используется подобных подход, но очень редко и чаще всего в аэрокосмической промышленности.

В дальнейшем было отмечено, что исчерпывающее тестирование практически невозможно.

В эпоху процессора 8086 тестирование стало подразумевать еще понятием «предупреждение дефектов». А с 1990-х годов в это понятие стали включать планирование, проектирование, создание, поддержку и выполнение тестов и тестовых окружений, и это означало переход от тестирования к обеспечению качества, охватывающего весь цикл разработки программного обеспечения.

Формальное определение

Очень часто от инженеров можно услышать определения, которые коверкают саму суть процесса:

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

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

Поэтому правильнее будет назвать следующее определение:

Тестирование это процесс выполнения программы с целью обнаружения ошибок.

Виды тестирования (по знанию устройства ПО)

Тестирование методом «черного ящика»

Или другими словами «тестирование, управляемое данными» (data-driven testing).

В соответствии с этим методом программа рассматривается как черный ящик, внутреннее поведение и устройство которого не имеет никакого значения.

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

Тестирование методом «белого ящика»

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

Это определение нельзя назвать полным, поскольку подобранный набор данных не всегда соответствует спецификации, но это наиболее полное определение, которое дает представление о тестировании методом «белого ящика».

Виды тестирования по уровню изолированности компонент

Конечно же здесь перечислены не все виды тестирования и это следует понимать. Остановимся лишь на тех этапах тестирования, которое чаще всего применяется в веб-разработке.

Блочное тестирование (юнит-тестирование)

Этот аспект проверяет самые мелкие аспекты работы системы. Например корректность каждого метода класса или отдельной функции.

Также отдельные аспекты проверки логики работы модулей входят в эту фазу тестирования. Например проверка функционала регистрации\авторизации пользователя на уровне объекта «пользователь».

Этот этап является проверкой методом «белого ящика».

Интеграционное тестирование

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

Функциональное тестирование

Этот этап тестирование является тестированием методом черного ящика. Этап проверяем отсутствие ошибок в реализации спецификации ПО. На входы форм подаются значения, и выполняется проверка данных на выходе тем, что описаны в спецификации.

Приемочное тестирование

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

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

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

Этот вид тестирования так же является тестированием методом «черного ящика».

Литература

  1. Книга «Искусство тестирования программ» Гленфорд Майерс, Том Баджетт, Кори Сандлер, ISBN 978-5-8459-1974-8
  2. Wikipedia: Software testing

Оглавление

Разработка, HowTo

PHP: дело о загадачном пробеле

%d0%b2%d1%8b%d0%b4%d0%b5%d0%bb%d0%b5%d0%bd%d0%b8%d0%b5_103Или история о том, как побились картинки.

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

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

Чтож. Очень вероятно, что какой-то вредный скрипт оказался с пробелом. Мы же помним вро стандартные открывающиеся и закрывающиеся теги php, которые принесли много головной боли.

Но как среди тысяч строк кода выяснить, где начинается вывод злобного пробела?

  1. отключить вывод самой картинки и убедится, что пробел все еще выводится.
  2. отключить буферизацию вывода на стороне сервера — убедится, что пробел есть
  3. в самом конце скрипта поставить 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
Обработка изображений, Разработка

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 и масштабируется).

Выделение_001

А свойства naturalWidth/naturalHeight не поддерживаются в ряде браузеров. поэтому способ универсальным назвать нельзя.

Разработка, HowTo

PHP: array_map и ключи массива

Интересно, почему такой вопрос часто всплывает на форумах или где-то еще?

Сначала постараюсь ответить на вопрос «нафига?», а потом на вопрос «как?».

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

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

Этот постыдный момент из жизни структуры привел к тому, что одним из самых частых применений массивов (в пхп конечно же) стало создание отображений категория=>какое-то значение.

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

Т.е. показать пользователю сам параметр, а скобках его категорию.

Как мы делаем это в языках (фреймворках), которые разделяют понятие массива и хеш-таблицы? Перебираем объект, который содержит категории и формируем по нему массив выводимых данных. Наверное так.

И тут мы открываем руководство по php и видим, что она применяет некий колбек ко всем элементам массива и после этого возвращает новый массив.

Удобно же! Применили колбек к массиву и получили обработанный.

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

var_dump(array_map(function(....

И что-то не заладилось. 🙂 Ключи в колбек не попадают. Можно сделать все через foreach, но тогда нам потребуется еще одна переменная. А тут все было просто и наглядно.

И мы идем в гугель: «php array_map with keys».

А зачем гуглить-то?

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

var_dump(array_map(function($key, $value) {
  return "{$key} => {$value}";
}, array_keys($array), $array));

И все отлично работает. И притом правильно. Относительно конечно же. И никаких вам лишних переменных.

А тем временем в коде вновь и вновь появляются конструкции вида

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

$description = [];

foreach ($array as $key => $value) {
  $description[] = "{$key} => {$value}";
}

Или еще хуже.

$array = [
  'category1' => 'first category',
  'category2' => 'second category',
];

$description = [];

array_walk($array, function($value, $key) {
  $description[] = "{$key} => {$value}";
});

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

Вариант же с array_walk на понятность никак не претендует (как и решение подобной задачи при помощи .each в jquery).

Разработка, HowTo

Git: откат мержа, который не был запушен

Иногда мы делаем мерж некоторой ветки в master и случается такое, что нам нужно по какой либо причине откатить ветку (мы не делали push еще в удаленный репозитарий), либо просто пытаемся сделать git pull забыв, что есть непрокоммиченые изменения.

При git pull мы увидим следующую картину

Git Pull Failed
You have not concluded your merge (MERGE_HEAD exists). Please, commit your changes before you can merge.

И чтобы откатить наш неудачный мерж не нужно делать никаких reset —hard HEAD=1.

Достаточно сделать

git reset --hard ORIG_HEAD

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