Как стать автором
Обновить

Комментарии 46

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

PS Кода для тестов получилось больше, чем самого исходника.
НЛО прилетело и опубликовало эту надпись здесь
Сам по себе конечно. Как и при помощи функционального все случаи не рассмотришь.

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

Ну и, наверное, это считается круто :)
НЛО прилетело и опубликовало эту надпись здесь
Ради рефакторинга тоже мотиватор :) Когда рефакторинг надо делать здесь и сейчас так, чтобы что-то не сломать.
Рефакторинг очень полезное занятие, но без тестов еще и очень опасное.

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

Не знаю как вы, а я скептически к тестам сейчас не отношусь. Видел насколько они бывают полезны.
НЛО прилетело и опубликовало эту надпись здесь
Абстрактный, да, не мотивирует. Но если следуете практикам TDD то рефакторинг у вас постоянный. Кроме того, юнит-тесты «провоцируют» создание архитектуры со слабой связанностью.
НЛО прилетело и опубликовало эту надпись здесь
юнит тестами (когда нет реальной БД и запросов к ней, но есть моки или стабы) покрываете паблик методы классов и интерфесов всеми возможными вариантами использования метода + все варианты неверных входных данных. При правильно организации, на 1 метод — 5-10 юнит тестов пишутся за пол часа и 1 раз.
И после этого ни вы ни ваша команда не имеет адских проблем с тем, что кто-то изменил метод и у всех упало и т.д. Тестируется только бизнес логика, а не связь с БД, логгирование и т.п. Это важно понимать.

А интеграционными тестами (когда есть реальная БД/сервис) проверешь 3-4 типовые ситуации, покрывающие запросы к БД или сервисам и реакцию системы на ответы/ошибки. Т.е. тестируется связка, а не логика.
И получаете + 30-50% увеличения времени на разработку и -150% лишних овертаймов, связанных с подобным родом ошибок и сложностью сопровождения и изменения.
Примерный итог: по времени не проигрываете случаю, когда сразу написали код, но потом увязли в багфиксинге и сопровождении
+ если метод нельзя покрыть тестом, то проблемы с архитектурой ( нарушение SOLID)
Это сугубо мой опыт.
То есть фактически, при разработке нового класса/метода он сразу же покрывается тестами (тут же проверятся, после завершения разработки класса/метода) и в будущем, при изменении этого класса/метода на тесты время не тратится и сразу же можно проверить правильность работы класса.

Я правильно понял?
Главная идея TDD — тесты пишутся даже раньше реализации метода. Т.е. вначале вы описываете тест (входные-выходные параметры, по сути), а затем реализуете код метода. Потом добавляете новый набор параметров, на которые ваша функция еще не рассчитана. И так до тех пор, пока вся необходимая функциональность не будет реализована.
Понятно. Спасибо за пояснение. Надо читать матчасть.
Я все понимаю, но такие статьи о тестировании арифметических операций уже поднадоели. Вот скажите честно, хоть раз такой тест пришлось писать? Или все-же чаще требуется тестировать более сложный код, например какие-то функции сервисного слоя (которые и БД используют)?
>хоть раз такой тест пришлось писать
Как начинал разбираться — да, писАл.
> например какие-то функции сервисного слоя (которые и БД используют)
Можно заменить в методах чтения/записи функции работы с файлами, на функции работы с БД.

В любом случае, главное — уловить суть. Я, например, долго не мог разобраться, по причине, что не мог найти руководство «для чайников». Сейчас уже вроде попроще, и потихоньку пишу тесты для своего проекта.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Хорошо получилось, намного более реальный пример, чем выше.

Опять же, очень полезны тесты при работе с двоичными файлами — высчитывания всех этих сдвигов, положений, и всего прочего в условиях изменяющейся структуры, трудно проверить «ручками»
НЛО прилетело и опубликовало эту надпись здесь
т.е. в Ruby есть возможность проверить вызов функции/метода з нужными аргументами без фактического выполнения его?
НЛО прилетело и опубликовало эту надпись здесь
Видимо вы говорите о runkit. Оно (расширение) уже вышло из статуса «хака» и вполне себе поддерживается как официальное PECL расширение. Все расширения так или иначе являются хаками ядра в широком смысле, но есть три ступени: «из коробки» (можно включить/отключит параметрами компиляции ядра, или даже отключить нельзя), PECL (официальный репозиторий расширений) и «хак» (проект не получивший официального одобрения и поддержки).
НЛО прилетело и опубликовало эту надпись здесь
Он нужен (в плане тестирования) только если стабить и мокать функции из глобального контекста или методы c захардкоженными зависимостями. Для методов в приложениях с архитектурой, активно использующей IoC, вполне хватает моков (а по совместительству и стабов) описанного в топике PHPUnit:
//...
    $sms_instance = $this->getMock('SMS', array('send'));
    $sms_instance
      ->expects($this->once())
      ->method('send')
      ->with($this->equalTo($batman->phone_number), $this->equalTo('some text'));

    $order = new Order($sms_instance);
//...

Ничего не напоминает? :)
В PHP тоже есть (для методов «из коробки»). Например www.phpunit.de/manual/3.7/en/test-doubles.html#test-doubles.mock-objects — работает (с некоторыми ограничениями) в том числе и над встроенными классами типа PDO или mysqli.

Для проверки и/или эмуляции вызова функций из глобального контекста есть расширение runkit, но обычно гораздо проще просто выделить вызов функции в метод объекта и применить IoC. Например, есть код (код для примера, никогда так не делайте):
class Users {
  public function getList() {
    $users = array();
    $result = mysql_query('SELECT * FROM users');
    while ($user = mysql_fetch_assoc($result) {
      $users[] = $user();
    }
  }
}

И где-то в тесте PHPUnit мы можем проверить его так:
//...
    $users = new Users();
    $user_list = Users->getList();
    $this->assertEqual($expected, $user_list);
//...

Вроде кажется, что без обращения к БД метод не протестировать, но можно сделать ход конём:
class MySqlDb {
  public function query($query) {
    mysql_query($query);
  }

  public function fetch_assoc($result) {
    mysql_fetch_assoc($result);
  }
}

class Users {
  private $db;

  public function __construct(MySqlDb $storage) {
    $this->db = $db;    
  }

  public function getList() {
    $result = $db->query('SELECT * FROM users');
    $users = array();
    while ($user = $db->fetch_assoc($result) {
      $users[] = $user;
    }
  }
}

И тест изменится на
//...
    $db = new MySqlDb();
    $users = new Users($db);
    $user_list = Users->getList();
    $this->assertEqual($expected, $user_list);
//...

Вроде ничего не изменилось, только кода больше стало, но теперь мы можем тестировать так (код приблизительный):
//...
    $db = $this->getMock('MySqlDb', array('query', 'fetch_object'));
    $db
      ->expects($this->once())
      ->method('query')
      ->with($this->equalTo('SELECT * FROM users'))
      ->will($this->returnValue(1275)); 
    $db
      ->expects($this->exactly(3))
      ->method('fetch_object')
      ->with($this->equalTo(1275))
      ->will($this->onConsecutiveCalls(
        array ('id' -> 1, 'name' -> 'Alice'), 
        array ('id' -> 2, 'name' -> 'Bob'), 
        false 
      ); 
    $users = new Users($db);
    $user_list = Users->getList();
    $this->assertEqual($expected, $user_list);
//...

без вызова БД!
Недавно разрабатывали систему по анализу поведения посетителей на сайте. Для этого был специальный движек которому скармливались выражения, и история действий посетителя. Выражения эти задаются пользователем, имеют вид
((a AND b) OR c) AND THEN NOT (e)
при этом существует множество модификаторов типа это случалось как минимум N раз в течении предидущего сеанса и длилось 15 секунд.
По отдельности это почти можно протестировать «руками», а вот все вместе… :)

В итоге за движком следили 165+ тестов, которые запускались после каждого изменения в системе и указывали если где-то что-то пошло не так.

О как я оценил пользу автоматического тестирования в то время, словами передать сложно.

И да, код тестов намного более громоздкий нежели самой системы. Хотя и более простой.
БД практически не тестирую (для этого Oracle есть :) ), тестирую, что мои методы вызывают нужные SQL выражения. Например, для примера в статьи, если считать что хранится в БД модель и получает соединение в конструктор, выйдет что-то вроде:
class TestModelTest extends PHPUnit_Framework_TestCase {

  public function testSaveDataCallDbCorrectWhenDataIsValid {
    // prepare valid data
    $num = 15; // between 10 and 20
    $str = 'something'; // non empty

    // prepare mock PDO object
    $db_connection = $this->getMock('mysqli', array('query')); // на самом деле конструктор сложнее
    $db_connection
       ->expects($this->once)
       ->method('query')
       ->with($this->equalTo("INSERT INTO tests (`num`, `str`) VALUES ($num, '$str')");

    // set data
    $model = new TestModel($db_connection);
    $model->setAttributes($num, $str);
     
    // test
    $model->saveData();
  }
}


Этим я проверяю, что вызов метода saveData для объекта с атрибутами 15 и something (то есть валидными) вызовет метод mysqli::query с параметром «INSERT INTO tests (`num`, `str`) VALUES (15, 'something')». Что такой вызов вставит в БД эти значения в юнит-тесте я не проверяю, доверяю Oracle и разработчикам PHP :)

Если же я пишу функциональный тест, то получается что-то вроде
class TestTest extends PHPUnit_Extensions_Database_TestCase {
  // куча инициализаций

  public function testAddTestFormHandlerSaveValidDataToDb {
    // prepare valid data
    $num = 15; // between 10 and 20
    $str = 'something'; // non empty
    
    // process data
    $request = new Request('POST', '/tests', array('num' -> $num, 'str' -> $str));
    $app =  new Application();
    $app->handle($request);

    // test
    $queryTable = $this->getConnection()->createQueryTable(
            'tests', 'SELECT * FROM tests'
        );
    $expectedTable = $this->createFlatXmlDataSet("expectedTests.xml")
                              ->getTable("tests");
    $this->assertTablesEqual($expectedTable, $queryTable);
  }
}


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

        $this->assertTrue($model->saveData());	//записали данные
        $this->assertTrue($model->loadData());	//прочитали данные


Если ваша функция loadData не будет ничего загружать тест всё равно пройдет. Ибо данные и так есть в объекте. Как минимум стоит загружать данные для другого объекта.
Ну случай правда выдуманный. Но замечание верное, спасибо. Правильно было бы создать новый объект. Сейчас поправлю.

> Если ваша функция loadData не будет ничего загружать тест всё равно пройдет
Если данные не загружены, то функция вернёт false, и тест не пройдёт.
function loadData() { return true; }

вот так пройдет. Также хорошо было б показать, что между тестами данные стоит очищать. Тест не должен «мусорить».
Davert очень-очень прав — специально для этого в PHPUnit-е есть методы setUp(), tearDown() и куча других
Хочется увидеть проверку того, что saveData() запишет файл именно нужного формата. Что, в частности, перевод строки там будет именно \r\n а не \n.

Ценность юнит-тестов, как я понимаю, еще и в том, что они ограждают от необдуманного изменения поведения функций. То, что они возвращают «все прошло хорошо» — конечно интересно, но это вершина айсберга.
require_once 'PHPUnit/Autoload.php';

Это не обязательно
Уважаемые хабраюзеры, сделайте уже статью чтобы там были рассмотрены реальные проекты, например на symfony2, yii, cakephp2… или еще лучше чтобы тестировались модули на drupal, joomla и так далее. С простыми тестами все итак давно понятно, хочется более сложной логики и более адекватных примеров.
ЗЫ: есть живой проект на друпале, готов научится писать тесты, если есть желающие помоч — велкам в личку.
Господи…

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

Вам нужно замокать поток, внутри мока сохранять что-нибудь типа лога вызовов и в тесте ассертить, что в логе правильный порядок вызовов. Тогда это будет юнит-тест.
а что такое мок, мокать? От английского mocking?
Ответил ниже.
> Вам нужно замокать поток, внутри мока сохранять что-нибудь типа лога вызовов и в тесте ассертить, что в логе правильный порядок вызовов. Тогда это будет юнит-тест.

Это, простите, как мне кажется, уже не укладывается в рамки «статьи для начинающих».
Простите, вполне укладывается.

Читайте Кента Бека «Разработка через тестирование», там все просто.
Такой чайниковый вопрос, если можно. При первом запуске тестов у Вас выводятся сообщения вида:
==
1) TestModelTest::testStringCannotBeEmpty
Failed asserting that null is false.

==
Так вот вопрос. Откуда взялся этот самый 'null'? Ведь, по сути,
вызов $this->assertFalse($model->saveData());
проверяет результат вызова метода saveDate(), который у нас возвращает false? Сам объект класса модели тоже создается ( $model=new TestModel;)
Просветите, плиз.
В php если метод явно не вернул никакого значения, то будет возвращен null в точку вызова.
Так в том-то и дело, что метод saveData() возвращает false:
==
public function saveData() {return false;}
==
Советую использовать аннотации /** @test */, а не префикс у метода.

Как правило пишу юнит тесты когда в методе много комбинаций данных — в функциях конвертирования данных, или когда математика важна (всякие цены, округления и тп.).

Интеграционные тесты — для веб-сервисов и БД.

Системные — для проверки работоспособности с UI.
pear config-set auto_discover 1
pear install pear.phpunit.de/PHPUnit

Очень идеализированный случай. Новички обычно используют вамп, денвер и т.п., где сначала нужно проапдейтить сам pear и еще кучу пакетов и только потом получиться поставить пхпюнит. А бывает, что один, из пакетов, на которых депендится пхпюнит, не имеет нужной релизной версии и необходимо переключить
pear config-set preferred_state beta
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории