Online video hd

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

Официальный сайт 4stupeni 24/7/365

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

16.06.10 02:45 idler

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

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


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

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



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



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

Предположим у нас есть класс Product, который умеет считать цену.
1
2
3
4
5
6
7
8
9
10
11

class Product
{
//...
  function getPrice()
  {
    return $this->incoming_price * 1.4;
  }
//...
}
 


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

1
2
3
4
5
6
7
8
9
10
11

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
19
20

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->assertEqual($p->getPrice(),$p2->getPrice());
    $this->assertEqual($p2->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
13
14

//...
  function testGetPriceWithDiscount()
  {
    $p = new Product(array('name'=>'testProduct','price'=>100));
    $p->save();
    $collection = Product::findByName('testProduct');
    $p2 = $collection[0];
    $this->assertEqual($p->getPrice(),$p2->getPrice());
    $this->assertEqual($p2->getPrice(),140);
    $this->assertEqual($p2->getPrice($percent_discount=5),133); // проверка прайса со скидкой.
  }
//...
 


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

Изменим код:

1
2
3
4
5
6
7
8
9
10
11

class Product
{
//...
  function getPrice($percent_discount=0)
  {
    return $this->incoming_price * 1.4 * 0.95;
  }
//...
}
 


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Product
{
//...
  function getPrice($percent_discount=0)
  {
    $price = $this->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
25
26

//...
  function testGetBasketTotalSum()
  {
    $p = new Product(array('name'=>'testProduct','price'=>100));
    $p->save();
    $p2 = new Product(array('name'=>'testProduct2','price'=>100));
    $p2->save();
    $collection = Product::findAll();
    foreach($collection as $prod)
    {
      Basket::add($prod,2); // 2 таких товара в корзину
    }
    $this->assertEqual(560,Basket::getTotalSum()); // тест провалится
  }
  function testGetAverageProductPrice()
  {
    $p = new Product(array('name'=>'testProduct','price'=>100));
    $p->save();
    $p2 = new Product(array('name'=>'testProduct2','price'=>100));
    $p2->save();
    $collection = Product::findAll();
    $this->assertEqual(140,$collection->getAveragePrice()); // тест провалится
  }
//...
 


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

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

//...
  function testGetBasketTotalSum()
  {
    $p = new Product(array('name'=>'testProduct','price'=>100));
    $p->save();
    $p2 = new Product(array('name'=>'testProduct2','price'=>100));
    $p2->save();
    $collection = Product::findAll();
    foreach($collection as $prod)
    {
      Basket::add($prod,2); // 2 таких товара в корзину
    }
    $this->assertEqual(532,Basket::getTotalSum()); // тест провалится
  }
 


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

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

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



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

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

1
2
3
4
5
6
7
8
9
10
 
function testGetPriceWithFivePercentDiscount()
{
 
    $p = new Product(array('name'=>'testProduct','price'=>100));
    $p->save();
    $p2 =  Product::getByName('testProduct');
    $this->assertEqual($p2->getDiscountedPrice(),133);
}
 


Сначала бывает что-то неуклюжее вроде приведенного выше кода. Вся фишка в том, что я уже пытаюсь использовать тот функционал, которого нету, и еще ДО реализации функционала вижу, что он неудобен! Далее делаю его удобней.
1
2
3
4
5
6
7
8
9
10
 
function testGetPriceWithDiscount()
{
 
    $p = new Product(array('name'=>'testProduct','price'=>100));
    $p->save();
    $p2 =  Product::getByName('testProduct');
    $this->assertEqual($p2->getDiscountedPrice($percent_discount=5),133);
    $this->assertEqual($p2->getDiscountedPrice($percent_discount=12),123.2);
}



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



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

Подводя итог


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



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


Посты Комментарии
Последние посты
    Посты Комментарии
    Последние комментарии
      Посты Комментарии
      Изменения
        Посты Комментарии Изменения Черновики Избранное
        Черновики (все)
          Посты Комментарии Изменения Черновики Избранное
          Избранное (всё)
            Посты Комментарии Изменения Черновики Избранное
            Лучшие блоги (все 125)
            Топ пользователей Топ блогов
            Топ пользователей Топ блогов
            Элита (все 2283 из 186 городов)
            Топ пользователей Топ блогов

            Новенькие: antime, lampslave, GrandMax, suguby, ignar
            welinux.ru

            Смотреть онлайн бесплатно

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


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

            Online video HD

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

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

            Full HD video online

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

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

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