Видео смотреть бесплатно

Смотреть узбек видео

Официальный сайт circ-a 24/7/365

Смотреть видео бесплатно

idler 16.06.2010 02:45

CodingUnit-тестирование

Модульное тестирование или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.


Модульное тестирование служит мне, как разработчику в достижении двух целей:

1. Определить интерфейс разрабатываемого класса еще до его реализации
2. Проверить, не привело ли добававление нового функционала к ошибкам в уже существующем коде



Отладчик больше не нужен?

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

Предположим у нас есть класс Product, который умеет считать цену.
1
2
3
4
5
6
7
8
9
class Product
{
//...
function getPrice()
{
return $this->incoming_price * 1.4;
}
//...
}



Код демонстрирует обычную магазинную накрутку в 40%. Предположим, что бизнес ставит задачу выводить на страницах сайта цены со скидкой в 5%.

1
2
3
4
5
6
7
8
9
class Product
{
//...
function getPrice()
{
return $this->incoming_price * 1.4 * 0.95;
}
//...
}



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

Все цены в админке выводятся на 10-20 рублей ниже, чем во внутренней базе!

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

СТОП!!!!

Давайте посмотрим как будут развиваться события при использовании модульного тестирования. До недавнего времени я пользовался SimpleTest для PHP и считаю, что PHPUnit стоит использовать только тогда, когда возможностей SimpleTest явно не хватает, потому примеры тестов будут для SimpleTest.


Тот код, который показан выше должен быть покрыт тестом приблизительно следующего содержания:
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Class ProductTest extends UnitTestCase
{

function setUp()
{
clear_and_init_database() ;
}

function testProductGetPrice()
{
$p = new Product(array('name'=>'testProduct','price'=>100));
$p->save();
$collection = Product::findByName('testProduct');
$p2 = $collection<0>;
$this-&gt;assertEqual($p-&gt;getPrice(),$p2-&gt;getPrice());
$this-&gt;assertEqual($p2-&gt;getPrice(),140);
}
}



Попробуем разобраться. Функция setUp выполняется перед каждым тестовым методом, потому в ней мы выполняем инициализацию БД, для того, чтобы в каждом тестовом методе иметь уверенность в новом чистом окружении. Однако стоит очищать окружение с умом: в некоторых тестах нужно чистое окружение, в некоторых специально подготовленное, в некоторых окружение вообще не важно. Вам решать - в каком окружении отрабатывать тесты, но чуть ниже я помогу с этим разобраться. Метод testProductGetPrice тестирует правильность отдаваемого классом значения - ключевыми здесь являются последние две строки. Если вы передаете в метод assertEqual два одинаковых значения - тест считается пройденым, но если значения не совпадают - тест провален и на экране красуется сообщение о несоответствии значений. assertEqual - не единственная проверка. Cуществует еще множество удобных проверок: assertTrue, assertFalse, assertNull, assertNotNull, assertPattern и т.д.

Перед началом изменения кода ( это я про скидку в 5% ) запускаем тесты и видим, что все в порядке. Теперь пробуем изменить код и видим, что наши изменения привели к ошибкам.

Мы конечно можем переписать тест, а далее все пойдет по тому же сценарию что и раньше, но я говорил, что пример надуман ради простоты. В более сложной ситуации вы не будете слепо менять метод и тест, понимая, что где-то getPrice может быть использован для получения РЕАЛЬНОЙ цены.

Лучше мы напишем еще один тест. Да да! Сначала тест, а потом код...
 1
2
3
4
5
6
7
8
9
10
11
12
//...
function testGetPriceWithDiscount()
{
$p = new Product(array('name'=&gt;'testProduct','price'=&gt;100));
$p-&gt;save();
$collection = Product::findByName('testProduct');
$p2 = $collection<0>;
$this-&gt;assertEqual($p-&gt;getPrice(),$p2-&gt;getPrice());
$this-&gt;assertEqual($p2-&gt;getPrice(),140);
$this-&gt;assertEqual($p2-&gt;getPrice($percent_discount=5),133); // проверка прайса со скидкой.
}
//...



Мы решили ввести параметр в метод получения прайса. Теперь запустим тест и получим Fatal error, т.к. метод объявлен без параметров.

Изменим код:

1
2
3
4
5
6
7
8
9
class Product
{
//...
function getPrice($percent_discount=0)
{
return $this-&gt;incoming_price * 1.4 * 0.95;
}
//...
}



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

Продолжим работу над кодом:

 1
2
3
4
5
6
7
8
9
10
11
12
class Product
{
//...
function getPrice($percent_discount=0)
{
$price = $this-&gt;incoming_price * 1.4 ;
if(!$percent_discount) return $price;
$coef = 1 - $percent_discount/100; // внимание, тут действуют особенности работы float в PHP, ради простоты я не стал от них избавляться
return $price * $coef;
}
//...
}



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

Казалось бы на выполнение задачи мы потратили больше времени, чем обычно, но поверьте - это иллюзия. В более сложной ситуации на поиск допущенной подобным образом ошибки может уйти пара часов, а то и дней. Модульные тесты сразу покажут, что последние изменения привели к ошибкам, и покажут к каким.

Вот еще пример теста, который провалится при подходе при прямом изменении метода:


 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//...
function testGetBasketTotalSum()
{
$p = new Product(array('name'=&gt;'testProduct','price'=&gt;100));
$p-&gt;save();
$p2 = new Product(array('name'=&gt;'testProduct2','price'=&gt;100));
$p2-&gt;save();
$collection = Product::findAll();
foreach($collection as $prod)
{
Basket::add($prod,2); // 2 таких товара в корзину
}
$this-&gt;assertEqual(560,Basket::getTotalSum()); // тест провалится
}
function testGetAverageProductPrice()
{
$p = new Product(array('name'=&gt;'testProduct','price'=&gt;100));
$p-&gt;save();
$p2 = new Product(array('name'=&gt;'testProduct2','price'=&gt;100));
$p2-&gt;save();
$collection = Product::findAll();
$this-&gt;assertEqual(140,$collection-&gt;getAveragePrice()); // тест провалится
}
//...



При упомянутых выше изменениях ( это снова про 5% ) эти два теста провалятся, НО testGetBasketTotalSum нужно будет переписать, а вот testGetAverageProductPrice должен остаться неизменным, но при этом должен нормально проходить.

Внутри метода Basket::getTotalSum() будет вызываться метод $product->getPrice(5) , потому нам нужно изменить тест:


 1
2
3
4
5
6
7
8
9
10
11
12
13
14
//...
function testGetBasketTotalSum()
{
$p = new Product(array('name'=&gt;'testProduct','price'=&gt;100));
$p-&gt;save();
$p2 = new Product(array('name'=&gt;'testProduct2','price'=&gt;100));
$p2-&gt;save();
$collection = Product::findAll();
foreach($collection as $prod)
{
Basket::add($prod,2); // 2 таких товара в корзину
}
$this-&gt;assertEqual(532,Basket::getTotalSum()); // тест провалится
}



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

Вот таким образом модульные тесты помогают избежать запуска отладки.

Проектирование интерфейсов

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

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

1
2
3
4
5
6
7
8
function testGetPriceWithFivePercentDiscount()
{

$p = new Product(array('name'=&gt;'testProduct','price'=&gt;100));
$p-&gt;save();
$p2 = Product::getByName('testProduct');
$this-&gt;assertEqual($p2-&gt;getDiscountedPrice(),133);
}



Сначала бывает что-то неуклюжее вроде приведенного выше кода. Вся фишка в том, что я уже пытаюсь использовать тот функционал, которого нету, и еще ДО реализации функционала вижу, что он неудобен! Далее делаю его удобней.
1
2
3
4
5
6
7
8
9
function testGetPriceWithDiscount()
{

$p = new Product(array('name'=&gt;'testProduct','price'=&gt;100));
$p-&gt;save();
$p2 = Product::getByName('testProduct');
$this-&gt;assertEqual($p2-&gt;getDiscountedPrice($percent_discount=5),133);
$this-&gt;assertEqual($p2-&gt;getDiscountedPrice($percent_discount=12),123.2);
}




Документация к коду

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

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


Тэги: development php Refactoring Unit tests
+ 16 -
Похожие Поделиться

cyrus 16.06.2010 03:10 #
+ 0 -
отлично
antigluk 16.06.2010 09:22 #
+ 0 -
Супер! Спасибо, тема модульного тестирования раскрыта =) !
m0nhawk 16.06.2010 10:07 #
+ 0 -
За статью большой Спасибо. Хотя сейчас мне было б интересней почитать про Unit testing на Python'e.
idler 16.06.2010 10:31 #
+ 0 -
Unit testing на Python мало чем отличается. Такие же тестовые классы, такие же фикстуры, такие же тестовые методы, такие же assert'ы.
К сожалению в Python я новичок и тестированием еще не пользовался, ибо на проектах из 2х-3х классов тестирование - как раз потеря времени :)

necrotigr 23.06.2010 21:40 #
+ 0 -
+1000
Очень хорошо, с примерами, и почти без привязки к языку.
Сам хочу юнит-тестирование использовать и постепенно к этому двигаюсь, и каждая такая статья приближает меня к заветной цели! :)
Спасибо ещё раз!

Смотреть видео онлайн

Онлайн видео бесплатно


Смотреть русское с разговорами видео

Online video HD

Видео скачать на телефон

Русские фильмы бесплатно

Full HD video online

Смотреть видео онлайн

Смотреть HD видео бесплатно

School смотреть онлайн