Эффективность автоматизации тестирования

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

Исторически сложилось, что автоматизацию часто рассматривают в сравнении с ручным тестированием, и потому, если у вас нет специфических задач, которые могут быть решены только с помощью автоматизации – сравнение имеет смысл.

Итак, автоматизацию в общей случае имеет смысл реализовывать если соблюдается условие:

TAcr/N + TAerrval + TAupd < TMval* N,
где
N – количество выполнений в течении одной итерации;
TAcr – время на создание автоматического теста;
TAerrval – среднее время на понимание причины “падения” авто-теста;
TAupd – среднее время на апдейт авто-теста;
TMval – среднее время ручной проверки.

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

Теперь становиться гораздо наглядней следующие моменты:

  • почему не выгодно автоматизировать функционал, который нужно проверить всего несколько раз;
  • почему выгодно автоматизировать регрессию;
  • почему тестирование API всегда более выгодно автоматизировать;
  • почему очень важно иметь высокий уровень абстракции в инструменте автоматизации;
  • почему так важно иметь иметь качественное логгирование результатов выполнения авто-тестов

Уменьшить время проверки можно эффективной схемой анализа результатов и дизайна авто-скриптов.
Ответы на следующие вопрос должны быть получены как можно ранее:

  • К какому функционалу каждая из ошибок принадлежит?
  • Какие действия выполнял скрипт в момент появления ошибки?
  • В каком состоянии было приложение и какими были динамические данные в момент выполнения авто-теста?

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

Как не нужно делать блоговые клиенты

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

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

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

Настройки IpTv через Wi-Fi. Odessa.tv

Рассказываю себе на будущее настройки для просмотра IpTv через WiFi для Asus WL-500P.
Как известно, Iptv передается по протоколу UDP, который валит вай-фай намертво. У меня прошивка “от Олега” – в нем встроен прокси UDP->TCP. Ip в локальную сеть маршрутизатора – 192.168.1.1

1. Зайти в админку рутера и настроить порт UDP 2 TCP (IP Config – Miscellaneous). Я поставил 7781.
2. В файле IpTvPlayer.m3u (плейлист со списком каналов, которые скачал у провайдера) поменял ссылки так:
было – udp://@225.1.2.16:1234
стало – http://192.168.1.1:7781/udp/225.1.2.16:1234
3. Заново загрузил список каналов в плеер – наслаждаюсь.

К сожалению, в HD качестве каналы до сих не отображаются :( не помогает ни увеличение кеширования ни прочие ухищрения. Буду думать дальше чо делать

Создание и восстановление бекапа mysql бд

Взял отсюда

Итак, работаем с командной строкой.

Экспорт базы данных в файл:
mysqldump -uUSER -pPASSWORD DATABASE_NAME > FILE_TO_SAVE_INTO,

где USER – имя пользователя базы данных, PASSWORD – пароль, DATABASE_NAME – имя базы данных, FILE_TO_SAVE_INTO – файл на диске, в который сливается дамп.

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

Импорт базы данных – обратная операция.
mysql -uUSER -pPASSWORD DATABASE_NAME < FILE_TO_RESTORE_FROM

параметры аналогичны предыдущему примеру с экспортом.

Теперь что касается архивов.
Нам понадобятся две операции – создание ZIP архива и его распаковка. Зачем нужны архивы? Представим, что сайт состоит из 5000 файлов. По фтп загружать его крайне неудобно и долго, один файл копируется в Х раз быстрее.

Создание ZIP архива:
zip -r ARCHIVE_NAME *

-r означает рекурсивно добавить все, что есть в данной папке в архив с названием ARCHIVE_NAME, где * – все что есть в текущей папке и ее подпапках.

Распаковка ZIP архива:
unzip ARCHIVE_NAME

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

Рекурсивная функция получения содержимого папки

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

// ------------ lixlpixel recursive PHP functions -------------
// scan_directory_recursively( directory to scan, filter )
// expects path to directory and optional an extension to filter
// of course PHP has to have the permissions to read the directory
// you specify and all files and folders inside this directory
// ------------------------------------------------------------

// to use this function to get all files and directories in an array, write:
//  = scan_directory_recursively('path/to/directory');

// to use this function to scan a directory and filter the results, write:
// $fileselection = scan_directory_recursively('directory', 'extension');

function scan_directory_recursively_backup($directory, $filter=FALSE)
{
    // if the path has a slash at the end we remove it here
    if(substr($directory,-1) == '/')
    {
        $directory = substr($directory,0,-1);
    }

    // if the path is not valid or is not a directory ...
    if(!file_exists($directory) || !is_dir($directory))
    {
        // ... we return false and exit the function
        return FALSE;

    // ... else if the path is readable
    }elseif(is_readable($directory))
    {
        // initialize directory tree variable
        $directory_tree = array();

        // we open the directory
        $directory_list = opendir($directory);

        // and scan through the items inside
        while (FALSE !== ($file = readdir($directory_list)))
        {
            // if the filepointer is not the current directory
            // or the parent directory
            if($file != '.' && $file != '..')
            {
                // we build the new path to scan
                $path = $directory.'/'.$file;

                // if the path is readable
                if(is_readable($path))
                {
                    // we split the new path by directories
                    $subdirectories = explode('/',$path);

                    // if the new path is a directory
                    if(is_dir($path))
                    {
                        // add the directory details to the file list
                        $directory_tree[] = array(
                            'path'    => $path,
                            'name'    => end($subdirectories),
                            'kind'    => 'directory',

                            // we scan the new path by calling this function
                            'content' => scan_directory_recursively($path, $filter));

                    // if the new path is a file
                    }elseif(is_file($path))
                    {
                        // get the file extension by taking everything after the last dot
                        $extension = end(explode('.',end($subdirectories)));

                        // if there is no filter set or the filter is set and matches
                        if($filter === FALSE || $filter == $extension)
                        {
                            // add the file details to the file list
                            $directory_tree[] = array(
                                'path'      => $path,
                                'name'      => end($subdirectories),
                                'extension' => $extension,
                                'size'      => filesize($path),
                                'kind'      => 'file');
                        }
                    }
                }
            }
        }
        // close the directory
        closedir($directory_list);

        // return file list
        return $directory_tree;

    // if the path is not readable ...
    }
    else {
        // ... we return false
        return FALSE;
    }
}

PHP, полезные ссылки

На ресурсах, представленных ниже можно найти множество реализаций тех либо иных функциональностей, DRY так сказать или “не изобретай велосипед” :)

  • http://snipplr.com/all/language/PHP
  • http://www.phpclasses.org/
  • http://www.codesphp.com/php-category

upd: сделал валидную rss ленту и прикрутил кросспостинг в Livejournal.com по расписанию. На очереди – кросспостинг в tumblr и массовый аплоад галерей с локальной машины.

Функция генерации валидного RSS

Написал функцию, генерирующую полностью валидный rss.xml
Константы берутся из конфига сайта. В rss ленту вставляется превью, помимо текста.

function rssToFile()
{
        $rssOutput = '< ?xml version="1.0" encoding="UTF-8"?>
        <rss version="2.0"
        xmlns:content="http://purl.org/rss/1.0/modules/content/"
        xmlns:wfw="http://wellformedweb.org/CommentAPI/"
        xmlns:dc="http://purl.org/dc/elements/1.1/"
        xmlns:atom="http://www.w3.org/2005/Atom"
        xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
        xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
        >
        <channel>
        <title>' . _SiteNameRu_ . '</title>
        <atom:link href="' . _BaseUrl_ . 'rss.xml" rel="self" type="application/rss+xml" />
        <link>'. _BaseUrl_ .'</link>
        <description>'. _SiteDescription_ .'</description>
        <copyright>Copyright 2011, '. _SiteName_ .'</copyright>
        <sy:updateperiod>hourly</sy:updateperiod>
        <sy:updatefrequency>10</sy:updatefrequency>';

        startup();
        $sQuery = "SELECT * FROM `Gallery` WHERE `gallery_status` = 'approved' ORDER BY `add_date` DESC LIMIT 10";
        $ResultSelect = mysql_query($sQuery) or die(mysql_error());

        while ($row = mysql_fetch_array($ResultSelect))
        {
            $id=$row['gallery_id'];
            $title=$row['gallery_seo_title'];
            $text=$row['gallery_seo_description'];
            $description = $text . "<br /><img src=\"". _BaseUrl_ . "gallery/thumbs150/" . $id . "/0.jpg\" width=\"150\"/><br />";

            $date=$row['add_date'];
            date_default_timezone_set('Europe/Berlin');
            $date=date("r", strtotime($date)); //. " GMT";

        $rssOutput .= "<item><title>$title</title>";
        $rssOutput .= "<description>< ![CDATA[". $description. "]]></description>
                <guid>" . _BaseUrl_ . "gallery/" . $id . "</guid>
                <pubdate>$date</pubdate>
                </item>";
        }
    $rssOutput .= '</channel></rss>';
    mysql_close();

    $fp=fopen('rss.xml', 'w+');

    if (!$fp) { echo "unable to open rss.xml"; }
    else
    {
        fwrite($fp, $rssOutput);
        fclose($fp);
    }

}

Работа в PHP с phpthumbs

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

function ImageResize2($type, $sFullSizeFile, $FileOutput)
{
    require_once('phpThumbs/phpthumb.class.php');
    $phpThumb = new phpThumb();

    $file = $sFullSizeFile;
    $phpThumb->setSourceFilename($file); 

    if($type == 0)
    {
        $phpThumb->setParameter('w', 150); // по ширине 150 пикселей
        $phpThumb->setParameter('zc', 1);  // делаем кроп по центру
    }

    if($type == 1)
    {
        $phpThumb->setParameter('w', 300);
    }

    if($type == 2)
    {
        $phpThumb->setParameter('w', 800); // по ширине 800 пикселей
        $phpThumb->setParameter('aoe', 0); // только уменьшаем!
    }    

    $outputFilename = $FileOutput;

    if ($phpThumb->GenerateThumbnail())
    {
        if ($phpThumb->RenderToFile($outputFilename))
        {
            // echo 'Success<br />';
        }
        else
        {
            // echo 'RenderToFile: failed<br />';
        }
    }
    else
    {
        // echo 'GenerateThumbnail: failed<br />';
    }
}

PHP: Функция рекурсивного удаления директории

Как аргумент передается полный путь к директории, к примеру “/var/www/site.ru/temp”

function deleteDirectory($dir)
{
    if (!file_exists($dir)) return true;
     if (!is_dir($dir) || is_link($dir)) return unlink($dir);
        foreach (scandir($dir) as $item) {
            if ($item == '.' || $item == '..') continue;
            if (!deleteDirectory($dir . "/" . $item)) {
                chmod($dir . "/" . $item, 0777);
                if (!deleteDirectory($dir . "/" . $item)) return false;
            };
        }
    return rmdir($dir);
}

PHP: Функция получения страницы сайта

Функция, использующая curl, которой я постоянно пользуюсь для парсинга страниц. К ней была написано небольшое дополнение, которое передает главную страницу копируемого сайта в качестве реферрера :)

function get_host($s)
{
    $s = preg_replace('#^http://#Uis', '', trim($s));
    $s = explode('/', trim($s));
    $s = trim($s[0]);
    $s = explode(':', $s);
    $s = trim($s[0]);
    return $s;
}

function get_web_page( $url )
{
    $uagent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)";
    $sRefferal = 'http://' . get_host($url);
    $ch = curl_init( $url );
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);   // возвращает веб-страницу

    curl_setopt($ch, CURLOPT_REFERER, $sRefferal);

    curl_setopt($ch, CURLOPT_HEADER, 0);           // не возвращает заголовки
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);   // переходит по редиректам
    curl_setopt($ch, CURLOPT_ENCODING, "");        // обрабатывает все кодировки
    curl_setopt($ch, CURLOPT_USERAGENT, $uagent);  // useragent
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 120); // таймаут соединения
    curl_setopt($ch, CURLOPT_TIMEOUT, 120);        // таймаут ответа
    curl_setopt($ch, CURLOPT_MAXREDIRS, 10);       // останавливаться после 10-ого редиректа

    $content = curl_exec( $ch );
    $err     = curl_errno( $ch );
    $errmsg  = curl_error( $ch );
    $header  = curl_getinfo( $ch );
    curl_close( $ch );

    $header['errno']   = $err;
    $header['errmsg']  = $errmsg;
    $header['content'] = $content;
    return $header;
}