Pull to refresh

Comments 46

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

PS Кода для тестов получилось больше, чем самого исходника.
UFO just landed and posted this here
Сам по себе конечно. Как и при помощи функционального все случаи не рассмотришь.

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

Ну и, наверное, это считается круто :)
UFO just landed and posted this here
Ради рефакторинга тоже мотиватор :) Когда рефакторинг надо делать здесь и сейчас так, чтобы что-то не сломать.
Рефакторинг очень полезное занятие, но без тестов еще и очень опасное.

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

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

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

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

В любом случае, главное — уловить суть. Я, например, долго не мог разобраться, по причине, что не мог найти руководство «для чайников». Сейчас уже вроде попроще, и потихоньку пишу тесты для своего проекта.
UFO just landed and posted this here
UFO just landed and posted this here
Хорошо получилось, намного более реальный пример, чем выше.

Опять же, очень полезны тесты при работе с двоичными файлами — высчитывания всех этих сдвигов, положений, и всего прочего в условиях изменяющейся структуры, трудно проверить «ручками»
UFO just landed and posted this here
т.е. в Ruby есть возможность проверить вызов функции/метода з нужными аргументами без фактического выполнения его?
UFO just landed and posted this here
Видимо вы говорите о runkit. Оно (расширение) уже вышло из статуса «хака» и вполне себе поддерживается как официальное PECL расширение. Все расширения так или иначе являются хаками ядра в широком смысле, но есть три ступени: «из коробки» (можно включить/отключит параметрами компиляции ядра, или даже отключить нельзя), PECL (официальный репозиторий расширений) и «хак» (проект не получивший официального одобрения и поддержки).
UFO just landed and posted this here
Он нужен (в плане тестирования) только если стабить и мокать функции из глобального контекста или методы 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; }

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

Ценность юнит-тестов, как я понимаю, еще и в том, что они ограждают от необдуманного изменения поведения функций. То, что они возвращают «все прошло хорошо» — конечно интересно, но это вершина айсберга.
Уважаемые хабраюзеры, сделайте уже статью чтобы там были рассмотрены реальные проекты, например на 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
Sign up to leave a comment.

Articles