Автоматическое тестирование и базы данных

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

    DbUnit


    Итак, DbUnit. Изначально был разработан для JUnit (фреймворк для юнит-тестирования Java-приложений) для сетапа БД перед запуском тестов. В итоге расширение развивалось и мигрировало и на другие фреймворки xUnit, в частности на PHPUnit. На данный момент поддерживаются MySql, PostgreSql, Oracle и Sqlite.

    Зачем DbUnit?


    Для тестирования взаимодействия Вашего приложения с базой данных Вам надо дополнительно проделать следующие шаги:
    • Учесть структуру БД и таблиц
    • Поризвести вставку первоначальных данных при необходимости
    • Провести проверку состояния БД после выполнения каких-либо операций бизнес-логики
    • Очистить базу и повторить заново для каждого теста (иначе последующие тесты будут зависеть от предыдущих)

    Если писать такое вручную с использованием SQL-запросов, то довольно скоро начинаешь проклинать юнит-тестирование в принципе. Кроме того, это не соответствует одному из главных принципов юнит-тестирования — тесты должны быть минимально сложны и максимально читабельны.

    По порядку


    Итак, как же правильно должен проходить тест взаимодействия с базой?
    1. Очистка базы. При первом запуске мы не знаем в каком состоянии находится БД, поэтому мы обязаны «начать с чистого листа»;
    2. Вставка начальных данных (фикстур). Обычно приложению нужны какие-либо начальные данные, которые оно извлекает из базы для последующей обработки. именно их и надо вставить в только что очищенную базу;
    3. Собственно выполнение тестов и проверка результатов. Без комментариев.

    PHPUnit Database Test Case


    Если в случае обычного тест-кейса в PHPUnit Вы должны просто унаследовать класс PHPUnit_Framework_TestCase, то в случае с тестированием БД все несколько сложнее:
    require_once "PHPUnit/Extensions/Database/TestCase.php";
    
    class MyTest extends PHPUnit_Extensions_Database_TestCase
    {
        public function getConnection()
        {
            $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
            return $this->createDefaultDBConnection($pdo, 'testdb');
        }
    
        public function getDataSet()
        {
            return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/guestbook-init.xml');
        }
    }
    


    Вы должны реализовать два абстрактных метода — getConnection() и getDataSet(). Первый необходим для установления соединения с базой, второй для заполнения базы таблицами и заполнения собственно таблиц.
    Важно заметить, что getConnection() должен использовать PDO для подключения к базе, но Ваше приложение не обязано использовать PDO для запросов к базе. Соединение, устанавливаемое методом getConnection() используется лишь для подготовки БД к тестам и ассертов.
    Начальное содержимое базы абстрагируется с помощью интерфейсов PHPUnit_Extensions_Database_DataSet_IDataSet и PHPUnit_Extensions_Database_DataSet_IDataTable. Метод getDataSet() вызывается методом setUp() для получения и вставки фикстур. В примере мы использовали фабричный метод createFlatXMLDataSet() для получения датасета из XML-представления.

    DataTables & DataSets


    Итак, что же это такое? Это ключевые понятия рассматриваемого расширения. DataTable и DataSet — это абстракция для таблиц и записей в реальной БД. Довольно несложный механизм позволяет скрыть реальную БД за объектами, которые в свою очередь могут быть реализованы различными способами.
    Такая абстракция необходима для сравнения ожидаемого контента базы и реального. Ожидаемый контент может быть представлен в различных видах благодаря абстракции — например, XML, CSV, массивы PHP. Интерфейсы DataTable и DataSet позволяют проводить сравнение данных из источника ожидаемых с реальными из БД.
    Также DataSet и DataTable используются для задания начального состояния базы данных перед выполнением теста.
    Ниже рассмотрим различные варианты датасетов.

    Flat XML DataSet


    Это наиболее простой вид датасета. Каждый элемент внутри корневого представляет собой одну запись из БД. Имя элемента должно соответствовать имени таблицы, а атрибуты и значения — поля и значения полей соответственно, например:
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
      <post post_id="1" title="My First Post" date_created="2008-12-01 12:30:29" contents="This is my first post" rating="5" />
      <post post_id="2"  title="My Second Post" date_created="2008-12-04 15:35:25"  contents="This is my second post" />
    </dataset>
    

    Это эквивалентно таблице post в БД с 2 записями
    post_id title date_created contents rating
    1 My First Post 2008-12-01 12:30:29 This is my first post 5
    2 My Second Post 2008-12-04 15:35:25 This is my second post NULL

    В общем-то, довольно просто и понятно.
    Пустая таблица эквивалента пустому элементу, например, пустая таблица current_visitors:
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
     <current_visitors />
    </dataset>
    

    NULL-значения для записи представляются как отсутствие соответствующего атрибута (см. пример с blog, поле rating), однако, тут следует учесть один момент. Для flat XML DataSet структуру таблицы определяет первый элемент, т.е. если в первом элементе нет каких-либо атрибутов, а в последующих элементах для той же таблицы они есть, то эти атрибуты будут проигнорированы. Например, если в примере с таблице blog из первого элемента убрать атрибут date_created с его значением, то во втором элементе этот атрибут не будет учитываться и в таблице не будет поля date_created.
    Использование с помощью метода createFlatXmlDataSet():
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function getDataSet()
        {
            return $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
        }
    }
    


    XML DataSet


    Данный вариант представления в XML лишен недостатков Flat XML, но и несколько сложнее:
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
      <table name="post">
        <column>post_id</column>
        <column>title</column>
        <column>date_created</column>
        <column>contents</column>
        <column>rating</column>
        <row>
          <value>1</value>
          <value>My First Post</value>
          <value>2008-12-01 12:30:29</value>
          <value>This is my first post</value>
          <value>5</value>
        </row>
        <row>
          <value>2</value>
          <value>My Second Post</value>
          <value>2008-12-04 15:35:25</value>
          <value>This is my second post</value>
          <null />
        </row>
      </table>
    </dataset>
    

    Таблица полностью представляется элементом <table>, в который вложены <column> для определения полей таблицы и <row> для представления записей. В свою очередь, в <row> могут быть вложены <value> для представления значащих полей и <null /> для NULL-значений.

    Пустая таблица представляется как таблица без <row> элементов:
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
      <table name="current_visitors">
        <column>current_visitors_id</column>
        <column>ip</column>
      </table>
    </dataset>
    

    Использование с помощью метода createXMLDataSet():
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function getDataSet()
        {
            return $this->createXMLDataSet('myFlatXmlFixture.xml');
        }
    }
    


    CSV Data Set


    Представление таблицы в формате CSV (Comma Separated Values — простейший формат для хранения таблиц). Все довольно понятно:
    post_id,title,date_created,contents,rating
    1,My First Post,2008-12-01 12:30:29,This is my first post,5
    2,My Second Post,2008-12-04 15:35:25,This is my second post,

    Использование несколько сложнее чем в случае XML:
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function getDataSet()
        {
             $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
             $dataSet->addTable('post', 'post.csv');
             return $dataSet;
        }
    }
    

    Для использования нам необходимо создать объект класса PHPUnit_Extensions_Database_DataSet_CsvDataSet. Конструктор принимает три аргумента, которые определяют формат CSV:
    public function __construct($delimiter = ',', $enclosure = '"', $escape = '"'){}
    

    После этого добавляем таблицы в датасет методом addTable — один файл — одна таблица.

    PHP массивы


    На данный момент нет стандартной реализации датасетов с помощью массивов, но ее нетрудно реализовать ;)

    Предположим, что нам надо хранить датасеты в таком формате:
    array(
                'post' => array(
                    array(
                        'post_id' => 1,
                        'title' => 'My First Post',
                        'date_created' => '2008-12-01 12:30:29',
                        'contents' => 'This is my first post',
                        'rating' => 5
                    ),
                    array(
                        'post_id' => 2,
                        'title' => 'My Second Post',
                        'date_created' => '2008-12-04 15:35:25',
                        'contents' => 'This is my second post',
                        'rating' => null
                    ),
                ),
            )
    


    Реализация:
    require_once 'PHPUnit/Extensions/Database/DataSet/AbstractDataSet.php';
    require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTableIterator.php';
    require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTable.php';
    require_once 'PHPUnit/Extensions/Database/DataSet/DefaultTableMetaData.php';
    
    class ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
    {
        protected $tables = array();
    
        public function __construct(array $data)
        {
            foreach ($data as $tableName => $rows) {
                $columns = array();
                if (isset($rows[0])) {
                    $columns = array_keys($rows[0]);
                }
    
                $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
                $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
    
                foreach ($rows as $row) {
                    $table->addRow($row);
                }
                $this->tables[$tableName] = $table;
            }
        }
    
        protected function createIterator($reverse = FALSE)
        {
            return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
        }
    }
    

    Немного комментариев — для своего датасета мы наследуем абстрактный датасет (который наследуют flat XML, XML, CSV и другие). В конструктор мы передаем оговоренный ранее массив. Как и в случае с flat XML струткура таблицы определяется первой записью, но в данном случае это не критично, потому что мы имеем возможность явно указать NULL-значения. Структура, кстати, определяется с помощью создания объекта PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData. После этого создаем собственно таблицу, передав в нее структуру и добавляем записи в таблицу с помощью метода addRow(). Так же нам необходимо реализовать абстрактный метод createIterator, но в этом нет ничего сложного :)

    Использование:
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function getDataSet()
        {
             return new ArrayDataSet(array(
                'post' => array(
                    array(
                        'post_id' => 1,
                        'title' => 'My First Post',
                        'date_created' => '2008-12-01 12:30:29',
                        'contents' => 'This is my first post',
                        'rating' => 5
                    ),
                    array(
                        'post_id' => 2,
                        'title' => 'My Second Post',
                        'date_created' => '2008-12-04 15:35:25',
                        'contents' => 'This is my second post',
                        'rating' => null
                    ),
                ),
            ));
        }
    }
    


    Query/Database Dataset


    Для ассертов нам потребуются не только ожидаемые датасеты, но и реальные из базы данных. В этом нам поможет QueryDataSet
    $ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
    $ds->addTable('post');
    

    или с явным использованием запроса:
    $ds->addTable('post', 'SELECT * FROM post ORDER BY post_id');
    

    Также можно использовать существующее подключение для автоматического получения датасетов из существующих таблиц с помощью метода PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection::createDataSet() (это объект, создаваемого в getConnection()). Если не передавать параметр в createDataSet(), то будет создан датасет из всех существующих таблиц. Если передать в качестве параметра массив с именами таблиц в базе, то датасет будет создан только из этих таблиц.

    Replacement DataSet


    Я уже упоминал о проблеме NULL-значений для flat XML датасета (для CSV проблема та же — невозможно явно задать NULL-значение в фикстуре). Это можно решить с помощью специального декоратора — ReplacementDataSet:
    require_once 'PHPUnit/Extensions/Database/DataSet/ReplacementDataSet.php';
    
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function getDataSet()
        {
            $ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
            $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
            $rds->addFullReplacement('##NULL##', null);
            return $rds;
        }
    }
    

    Теперь мы можем использовать ##NULL## в XML для обозначения NULL-значения:
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
      <post post_id="1" title="My First Post" date_created="2008-12-01 12:30:29" contents="This is my first post" rating="5" />
      <post post_id="2"  title="My Second Post" date_created="2008-12-04 15:35:25"  contents="This is my second post" rating="##NULL##" />
    </dataset>
    


    Фильтрация датасетов


    В случае больших датасетов можно применить фильтрацию с помощью DataSetFilter:
    require_once 'PHPUnit/Extensions/Database/DataSet/ReplacementDataSet.php';
    
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function testIncludeFilteredPost()
        {
            $dataSet = $this->getConnection()->createDataSet();
    
            $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
            $filterDataSet->addIncludeTables(array('post'));
            $filterDataSet->setIncludeColumnsForTable('post', array('post_id', 'title'));
            // ..
        }
    
        public function testExcludeFilteredPost()
        {
            $dataSet = $this->getConnection()->createDataSet();
    
            $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
            $filterDataSet->addExcludeTables(array('foo', 'bar', 'baz'));
            $filterDataSet->setExcludeColumnsForTable('post', array('date_created', 'rating'));
            // ..
        }
    }
    

    В первом случае мы оставили в датасете только таблицу post и содержимое ее записей только для полей post_id и title. Во втором — мы исключили из датасета таблицы 'foo', 'bar' и 'baz', а из записей таблицы post убрали значения для полей 'date_created' и 'rating'.

    Композиция датасетов


    Мы можем соединять несколько датасетов в один. В случае, если датасеты имеют одинаковые таблицы, то записи в них будут добавлены, например:
    dataset-1.xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
      <post post_id="1" title="My First Post" date_created="2008-12-01 12:30:29" contents="This is my first post" rating="5" />
    </dataset>
    

    dataset-2.xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <dataset>
      <post post_id="2"  title="My Second Post" date_created="2008-12-04 15:35:25"  contents="This is my second post" />
    </dataset>
    

    Аггрегируем их:
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function getDataSet()
        {
            $ds1 = $this->createFlatXmlDataSet('dataset-1.xml');
            $ds2 = $this->createFlatXmlDataSet('dataset-2.xml');
    
            $compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
            $compositeDs->addDataSet($ds1);
            $compositeDs->addDataSet($ds2);
    
            return $compositeDs;
        }
    }
    


    Asserts


    Часто бывает необходимо проверить количество записей в таблице. Это можно сделать с помощью обычного assertEquals:
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
       public function testAddEntry()
        {
            $this->assertEquals(2, $this->getConnection()->getRowCount('post'));
    
            $blog = new Blog();
            $blog->addPost("My third post.", "This is my third post.");
    
            $this->assertEquals(3, $this->getConnection()->getRowCount('post'));
        }
    }
    

    Метод getRowCount() возвращает количество записей в указанной таблице.

    Для сравнения таблиц используется метод assertTablesEqual():
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function testTables()
        {
            $queryTable = $this->getConnection()->createQueryTable('post', 'SELECT * FROM post');
            $expectedTable = $this->createFlatXmlDataSet("myFlatXmlFixture.xml")->getTable("post");
            $this->assertTablesEqual($expectedTable, $queryTable);
        }
    }
    

    Необходимо помнить о том, что тест может сфейлится при проверке даты — если у вас в фикстуре есть установленная дата, а в базу записывается текущее время, то Вы получите фейл, если эти даты не совпадают. Поэтому зачастую из ожидаемого результат убирают даты и, соответственно, изменяют получение реального датасета:
    $queryTable = $this->getConnection()->createQueryTable('post', 'SELECT post_id, title, date_created, contents, rating FROM post');
    

    И наконец, можно сравнивать непосредственно датасеты с помощью assertDataSetsEqual():
    class MyTestCase extends PHPUnit_Extensions_Database_TestCase
    {
        public function testTables()
        {
            $dataSet = $this->getConnection()->createDataSet(array('post')); 
            $expectedDataSet = $this->createFlatXmlDataSet("myFlatXmlFixture.xml");
            $this->assertDataSetsEqual($expectedDataSet, $dataSet);
        }
    }
    


    Статья во многом написана на основе статьи Benjamin Eberlei «The Ultimate Guide to Database-Testing with PHPUnit», ну и конечно официального мануала.
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 48

      0
      Тест работающий с базой данных не является юнит тестом, вы просто не сможете дождаться результата их работы. Назовите их функциональными тестами.
        +4
        То что тесты работающие с базой данных прям такие медленные, что их нельзя использовать это совсем не правда. Типичная страшилка. Они медленные только по сравнению с тестами, не работабщими с базой. Просто посчитайте: несколько инсертов, апдейтов и селектов средней сложности это десятки миллесекунд не больше (в тестовой БД, обычно данных нет, при каждом тесте она обнуляется, или просто каждый тест идёт в отдельной транзакции). Оверхед на один тестовой пример небольшой, и хотя примеров много, в целом разница не смертельная, это еденицы минут для очень больших тестовых пакет и секунды, десятки секунд для небольших и средних проектов. Я не говорю что все тесты, должны работать с бд. Если в тесте можно обойтись без неё, то нужно обойтись. Но то как проект работает с БД тоже надо тестировать. И в нормальном тестовом пакете всё равно придёться иметь такие тесты, иначе регрессивность пакета будет никуда не годной.

        P.S. А если тесты запускать на БД в памяти то оверхед вообще минимальным будет.
          –1
          это теория, на практике тесты базы ставят крест на юнит тестировании уже через несколько недель после начала разработки
            +2
            Да бросьте вы уже людей пугать. Что это за страшные тормоза от использования базы в текстах? Тестовая база на то и тестовая, чтобы быть пустой, маленькой и юркой. Ну и таблицы, расположенные в памяти, говорят, придумали.
            Можно пример случаев. когда оно тормозит. Только не высосанных из пальца, а действительно актуальных?
              +3
              У меня абсолютно другой практический опыт. Как и у многих других в rails сообществе. Сейчас, наоборот, всё больше говорят, что предпочтительно тестировать не стабя вызовы к БД. Выгода от менее хрупких и более надёжгых тестов стоит потерь скорости.
                0
                ~1k тестов для Doctrine, примерно половина из которых работают с БД, выполняются за ~15 секунд на обычной машине.
            +4
            А можно глупый вопрос — зачем?
            Чем проверка
            $this->assertEquals(3, $this->getConnection()->getRowCount('post'));

            лучше проверки
            $this->assertEquals(3, $blog->getPostsCount());

            ?
              0
              Объект $blog может ошибаться.
                0
                Это как?
                Всегда возвращать 3? Проверяем исходное количество записей, добавляем еще одну, проверяем, что количество записей увеличилось на 1.
                Не сохранять в базе? А мне пофигу ГДЕ он хранит, мне нужно чтобы он хранил.
                   $blog = new Blog();
                   $this->assertEquals(0, $blog->getPostsCount());
                
                   $blog->addPost("My third post.", "This is my third post.");
                   $blog->save();
                
                   $loaded_blog = Blog::loadById($blog->getId());
                   $this->assertEquals(1, $blog->getPostsCount());
                
                  0
                  Если заново перезагружать объект, то ничем не лучше, по сути. Если не перезагружать, то объект может ошибаться (добавить пост во внутреннюю коллекцию, но запрос на вставку сфейлится, например)
                    +1
                    Лучше тем, что тестируется поведение, а не внутренняя реализация. Ну и отсутствием лишней библиотеки.
              +3
              Вот так вот и плодятся массовые заблуждения относительно юнитетестов, из которых потом вырастает убежденность что модульное тестирование это «долго и дорого».

              Контролллер -> Фасад -> микс из доменных объектов и сервисов -> Репозиторий/ДАО ->реляционнаясубд/key-value хранилище/in-memory/ file/etc

              Так вот юниттест пишется Для! интерфейса! вашего хранилища/dao. Он независим от вашей реализации и работает всегда одинаково. (И когда вы смените реализацию — поможет убедиться что ничего не поменялось) Почти все тесты для него пишут так же как в младших классах нас учили проверять результаты вычислений: результат умножения проверяется делением, сложения — вычитанием. Ну или чуть более формально: f^-1(f(A))==A

              Для тестирования «другой» стороны — используются разного рода «пустышки».
              И всё. Никаких БД. (если конечно это МОДУЛЬНОЕ тестирование)
                0
                Весьма верное замечание. Позвольте пожать вашу руку, коллега!
                  0
                  Я согласен. Но, бывают исключения.
                  Например, то, почему я связался с юнит-тестированием БД. Есть система, написанная и спроектированная в ~2004 году без всяких DAO. Система большая и работает (с костылями, но работает). Нас, как разработчиков она не устраивает, но она устраивает бизнес, потому что _работает_. Времени и средств на переписывание с нуля естественно никто не выделяет по причине «оно и так работает». Мы хотим «переписать» систему своими силами в процессе выполнения тасков. Для этого нужно покрыть все юнит-тестами.
                    0
                    Вам важен факт факт, что она работает, или как столбцы называются? Зачем вводить зависимость от SQL?
                      +2
                      у них другая проблема.
                      У них есть монолитная система. Скорей всего — достаточно сложная (простую систему можно было бы быстро переписать).
                      Есть потребность навести в системе порядок. Хотя бы минимальный.
                      Каждое изменение сопряжено с появлением новых ошибок (много связей + языки с динамической типизацией помогают).
                      Единственные стабильные интерфейсы в такой ситуации это со стороны пользователя (напр. контроллеры в MVC) и со стороны базы. Все остальные интерфейсы будут меняться в процессе рефаткоринга.
                      Вот и они и стремятся написать автотесты через те стабильные интерфейсы что у них есть, чтобы во время рефаткоринга получать информацию что «вроде работает так же ка краньше». Других враиантов кроме как «с базой» у них особо нет.

                      просто такие тесты — совсем не «юнит».
                        0
                        Это же веб, т.е. уже не полностью монолит. Я бы начинал сверху, с уровня контроллеров/экшенов и спускался вниз, выделяя слои. В любом случае — контроль над базой им особенно ничего не даст, кроме документирования работы с базой текущего кода.
                        Хотя согласен, это будет полезно, если переписывать код целыми модулями. Если же править итерационно, то не вижу плюсов.
                          0
                          Ну представте, что у вас и «бизнесс логика» закодирована прямо в экшенах контроллера и всё это выглядит например так:

                          валидация данных из $_POST
                          чтение чегото из базы (вот прямо mysql_query)
                          какие-то манипуляции с прочитанными данными и данными из базы (прямо здесь же. в лучшем случае в приватных методах/функциях)
                          запись данных в базу (всё тем же mysql_query)
                          вывод результатов (часто с ипользованием чего-нибудь типа Smarty) )

                          В такой ситуации просто нет других интерфейсов на которые можно было бы опереться при тестировании
                            0
                            Вы же сами говорили о проверках умножения, через деление. Находим проверочное действие, глобально отметаем представление, подменой Smarty-объекта на свой, тестируем связку действий. Интерфейсов как минимум два — http и смарти. Контроль над базой не даст протестировать чтение, только create/update/delete.
                              +1
                              в том контексте который я описал выше — всё становится с ног на голову)
                              в модульном тестировании — всё достаточно просто и тестировать работу операций действительно удобно попарно с их обратными «функциями». Но такие случаи — не модульное тестирование)

                              Когда у нас есть огромный кусок кода, у которого много обязанностей (да и чего уж там — иной раз просто непонятно что этот кусок в точности делает) — тут тестировать обратными функциями конечно можно, но это стоит дороже.
                              Нам нужно добавить например 100 сущностный что бы проверить операцию чтения. Да причём ещё не рэндомных — а сущностей с некоторыми условиями. А когда вы не знаете что именно делает этот код — это становится сложно — требуется сначала разобраться, затем или написать какой-то генератор или руками нагенерить данные. Это всё требует времени и так же сопряжено с ошибками.

                              С другой же стороны — у нас уже есть набор данных в базе — на котором работает наша система. Есть собственно сама рабочая система. В данной ситуации выглядит более разумным сделать примерно следующее:

                              0)«Оторвать» представление дабы мы могли внедрить туда нужный нам объект.
                              1)Делаем дамп боевой базы
                              2)ждём 1-2-3 дня (попутно собирая подробные аксеслоги. не забываем про параметры POST)
                              (3) опционально -Делаем новый дамп базы.

                              Далее делается 2 тестовых инстанса:
                              1)работающая сейчас система
                              2)система на которой мы проводим рефакторинг

                              каждой из них даётся слепок базы из п. (1)
                              и начинают накатываться запросы из аксеслога (оригинальный или модифициорованный)

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

                              как вариант используем дамп (3) и сравниваем с тем что получилось у нас. (эффекты связанные со временем или игнорируются или учитываются специальным образом по мере обнаружения) )

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

                              вот как-то так.
                              ну и ещё раз — это не модульное тестирование и здесь «всё по другому»)
                                0
                                Ну да, фиксация состояия, я уже выше согласился))

                                В такой же ситуации я начинал с пустой(ну кроме словарей) базой, и прошелся по всем прецедентам, логгируя связки action -> {sql-запросы}. После недолгой медитации над результатами, удалось узнать возможные side-эффекты от изменений. Собственно отсюда и были получены связки action'ов, которые можно тестировать друг другом.

                                И все равно я не понимаю зачем эта библиотека, если мы оба говорим о слепках :)
                                Равное тестируют, либо равным, либо более низкоуровневым. Иметь еще одну библиотеку для работы с sql? Плюсы? Смены БД на непонятном legacy-коде не будет. Банальное неудобство mysql_*? Есть PDO. Дампы проще снимать и применять в SQL. Да и читается он лучше, чем XML.
                                –1
                                Почему не даст? Заносим в базу данные из фикстур, выполняем «юнит» с функцией read, проверяем его результат на соответствие тому, что занесли (вплоть до проверки через DOM и/или регулярками выходного html, захваченного через ob_*), чистим базу.

                                Сам сейчас начал рефакторить очень монолитный проект: каждый «экшн» это обычный спагетти-код, где вывод html и логика перепутаны, нет даже намёка на отделение логики от представления, почти каждый экшен начинается с <html> и заканчивается </html>, инклудится, по сути, только mysql_connect и mysql_select_db. Изменения в дизайн приходится вносить правкой в сотне файлов. Естественно первый позыв вынести хотя бы общий для каждого экшена код (прежде всего html c хидерами/футерами) за скобки, но хочется быть уверенным, что ничего не сломалось в процессе выноса (в сотне файлов проделать десяток операций по копипасту очень занудно, и легко что-то забыть из-за отупления).
                                  –1
                                  Всю эту штуку использовать только для заливки фикстур? Как-то расточительно ;)
                                    0
                                    Что значит только для заливки текстур? Стандартный xUnit воркфлоу — подготавливаем юнит (заливаем в базу фикстуры, устанавливаем переменные и т. п.), выполняем юнит (инклудим скрипт, перехватывая вывод, стандартный PHPUnit_Extensions_OutputTestCase мне неудобным показался, потому тупо через ob_* перехватываю), затем ассерты (в основном assertSelect* и assertTag) и очистка.

                                    А уже покрыв «юнит» (скрипт) тестами, можно спокойно начинать его рефакторить, при этом изначальные тесты служат в качестве интеграционных.
                                0
                                Если бы представление было хотя бы на смарти, было бы намного легче. То, что сейчас у нас, лучше не показывать неподготовленному человеку, знакомому хоть немного с MVC (:
                                  +1
                                  Как я вас понимаю… Код, с которым сейчас работаю, лучше не показывать даже тем, кто хоть как-то привык повторно использовать код, даже презираемые многими include 'header.phtml';… include 'footer.phtml' показались бы мне, наверное, идеалом кодирования
                          +2
                          Да. Всё верно.
                          Но просто дело в том что в вашем случае это не ЮНИТ-тесты.
                          Плохо спроектированные системы не поддаются модульному тестированию. Автотесты которые при определённых усилиях можно для таких систем написать — как правило большие, сложные (сложность часто сравнима с тестируемой подсистемой) и очень хрупкие. Они долго пишутся, часто исправляются.
                          Это может называться функциональными или интеграционными тестами в зависимости от масштаба бедствия. но никак не модульными.
                          юнит-тест является автотестом. обратное верно не всегда. И распространение материалов в которых автотесты «вообще» названы модульными — формирует неправильное представление у наших с вами коллег вообще (и связаных с php в частности) и в конечном итоге способствует увеличению числа проектов вроде того в котором оказались вы.

                          Кстати — не думали о смене места работы?
                          Учиться новому и перенимать лучше практики — луче же чем исправлять чужие ошибки. Тем более что такая работа часто бывает невидна и непонятна «обычным людям» (читай представителям бизнеса)
                            0
                            Спасибо за Ваши комментарии, они действительно в самую точку :) Название статьи я подправил.
                            Касательно смены работы — думал об этом раньше, сейчас у нас наметились сдвиги в лучшую сторону, и очень хочется поучаствовать в этом :) ну а best practices хватает благодаря сторонним проектам на zf и magento.
                          0
                          Да, большинство сервисов должно тестироваться с застабленными репозиториями, но ведь это не отменяет необходимости интеграционного тестирования, когда те же сервисы работают с реальной базой. По сути, единственная ошибка автора в том, что он интеграционные тесты назвал юнит-тестами.
                            0
                            да. полностью согласен. и собственно об этом я и написал.
                            О терминологической неточности которая вводит в заблуждение коллег по цеху.
                          0
                          YAML либо JSON хорошо подходит для описания данных. Таблицы типа memory в MySQL отстреливают ОЧЕНЬ быстро, нам не надо мучать диск. Кстати — пока неясно как через такие описательные языки создавать внешние ключи и вязать таблицы, поэтому мы покаструктуру накатываем простыми SQL, а вот данные грузим из YAML.
                            0
                            Механизм fixtures описанный в этой статье это плохо. Фикстуры ненадёжны и неудобны. Это поняли ещё в 80-х. Rails сообщество поняло пару лет назад. Php ещё нет :-). Надо пользоваться фабриками. Не знаю как с ними обстоит в php, видимо, плохо.
                              0
                              С таким же успехом можно сказать, что РНР сообщество поняло это 5 лет назад — wiki.agiledev.ru/doku.php?do=revisions&id=tdd:object_mother
                                0
                                Это понял автор статьи. Раз пока нет широкоиспользуемых решений для этого подхода, значит пока не поняло.
                                  0
                                  А какие решения? Вся суть паттерна: не хочешь дублирования бизнес-правил в снимке базы и коде — используй бизнес код для наполнения данных. Ну можно генераторы строк туда прекрутить. Генераторы файлов. А в остальном то все руками пишется.
                                    0
                                    А это только так кажется, на самом деле, если писать создание тестовых объектов руками, то будет много дублирования. Сложный объект не просто создать, у него много связей. А что если какие-то аттрибуты у модели уникальны: вручную следить за их уникальностью?
                                    Без factory_girl (или чего то такого) github.com/thoughtbot/factory_girl мне было бы очень трудно. Например, можно создать базовое правило производства объекта, а затем конкретизировать его в новом правиле, наследовав от старого.
                                      0
                                      Как то так?
                                      function createPersonWithJob($finance)
                                      {
                                        $person = $this->createPerson($dob = null, $with_passport = false, $name = null, $finance);
                                        return $person;
                                      }
                                      
                                    0
                                    $this->creator->createAndLoginUser()
                                    
                                    Не знал, что это называется ObjectMother. Мы пришли к подобному паттерну, пытаясь абстрагироваться от часто повторяемых в тестах пользовательских сценариев (залогиниться, создать документ и т.п.). Поэтому называем такие фабрики UserScenario.

                                    Только вот на душе остается какой-то не хороший осадок. Ведь эти «мамы», для того, чтобы рожать каких-то, прямо скажем, незапланированных детей, начинают «залетать» изрядными кусками сервисной логики. Причем эти куски, по своему характеру, очень напоминают соседей из business-logic — хоть тесты на них самих теперь пиши.
                                      0
                                      А вот этого не должно быть. С какими частями дублируется код? С контроллерами?
                                        0
                                        Например, сценария createAndLoginUser() в реальном коде не существует — потому, что пользователей заводит администратор, но ни когда их не логинит. Логиняться пользователи всегда сами, но только в существующий аккаунт.

                                        Однако в тестах очень часто участвуют именно такие вот залогиненные пользователи, поэтому от деталей их создания и логина хочется абстрагироваться. Думаю, такой код мог бы быть контроллером (если бы необходимость в такой логике, по факту, была не только в тестах).
                                          0
                                          А в чем проблема. Код создания пользователя есть, код логина — тоже. Остается объединить. В идеале:
                                          function createAndLoginUser($params = ...)
                                          {
                                            $auth_manager = new AuthManager($this->createUser($params));
                                            $auth_manager->login();
                                          }
                                          

                                          У нас, кстати, loginByUser используется повсеместно. Администратор может залогиниться под любым пользователем. Помогает при расследовании баг-репортов.
                                            0
                                            Значит в вашей системе эта логика является вполне нормальной — бизнес- — наверняка, окруженная полноценными тестами, документацией и проч. семейной атрибутикой. Нашим администраторам такая возможность не требуется. В итоге аналогичный код одиноко подвисает в слое тестов, в виде абсолютно неконтролируемой логики (чуть не сказал — женской). Получается, что интерпретация зависит от конкретного набора требований.

                                            Так вот проблема — нужно-ли писать тесты для MotherObject или нет? :) Голос «за» — они рожают точно такие же объекты, какие появляются на свет у нормальной бизнес-логики (просто какими-то левыми сценариями). Голос «против» — кажется, это будут тесты, ради тестов.
                                              +2
                                              По идее, логика ObjectMother должна быть тривиальной — либо простое конструирование объекта(ов) и установка его(их) атрибутов, либо обращение к уже протестированным конструкторам, сеттерам, фабричным методам и т. д. нормальной бизнес-логики, ветвления там, по-моему, неуместны, линейное выполнение, разве что в примитивный цикл свёрнутое. А появляться «мама» должна в процессе рефакторинга работающих тестов (опять-таки тривиального рефакторинга), а значит тестами для «мамы» будут служить и уже имеющиеся тесты, и тестируемая логика. Неправильно написали «маму» — тесты посыпятся, они из «юнит» превращаются как бы в интеграционные («мамы» и тестируемого юнита), но раз логика мамы тривиальна, то фактически остаются юнит-тестами юнитов :). Как-то так, по-моему.
                                                0
                                                Спасибо, теперь понял.
                                  0
                                  >Rails сообщество поняло пару лет назад.

                                  Наверное именно поэтому в гайдах к рельсам фикстурам выделена треть главы, а Factory Girl и
                                  Machinist по строчке :)
                                  0
                                  Когда таблица оказывается пустая, DbUnit вываливается с ошибкой. github.com/sebastianbergmann/dbunit/issues#issue/11

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

                                  И еще. Большой косяк вылазит на практике, когда у вас есть колонки типа timestamp или каким либо другим образом скрипты каждый раз вставляют туда разные даты, например, если вы тестируете регистрацию пользователей.
                                    0
                                    О проблемах со временем я упомянул практически в самом конце :)
                                    0
                                    С виду такая хорошая, годная статья, но такая длинная что я уже третий день не могу ее начать читать.

                                    Only users with full accounts can post comments. Log in, please.