Юнит-тестирование в PHP

    Язык PHP очень легок для изучения. Это, а так же обилие литературы «Освой _что_угодно_ за 24 часа» породило большое количество, мягко говоря, некачественного кода. Как следствие, рано или поздно любой программист, который выходит за рамки создания гостевой книги или сайта-визитки сталкивается с вопросом: «а если я здесь немножко добавлю, все остальное не ляжет?» Дать ответ на этот вопрос и на многие другие может юнит-тестирование.

    В самом начале хочется оговориться — здесь речь не будет идти о TDD и методологиях разработки ПО. В данной статье я попробую показать начинающему PHP-разработчику основы использования модульного тестирования на базе фреймворка PHPUnit


    Вместо предисловия


    Вначале возникает вполне резонный вопрос: А зачем, если все и так работает?
    Здесь все просто. После очередного изменения что-то перестанет работать так как надо — это я вам обещаю. И тогда поиск ошибки может отнять очень много времени. Модульное тестирование может свести этот процесс к считанным минутам. Не будем так же исключать переезд на другую платформу и связанные с ним «подводные камни». Чего только стоит разность в точности вычислений на 64- и 32-разрядных системах. А когда-то вы в первый раз столкнетесь с нюансами вроде

    <?php
    print (int)((0.1 + 0.7) * 10);
    // думаете 8? проверьте ...
    // или лучше так: "Вы еще не используете BC Math? Тогда мы идем к вам!"



    Если вопрос необходимости отпал, то можно приступить к выбору инструмента. Здесь ассортимент в общем-то невелик — PHPUnit (http://www.phpunit.de/) и SimpleTest (http://www.simpletest.org/). Поскольку PHPUnit уже стал стандартом де-факто в тестировании PHP приложений, то на нем и остановимся.

    Первое знакомство


    Вопросы установки рассматривать не будем: все достаточно внятно изложено на сайте. Попробуем лучше понять, как это все работает.

    Допустим, есть у нас некий класс «MyClass», одним из методов реализующий возведение числа в степень. (Здесь вынужден извиниться, но все примеры, в общем-то, высосаны из пальца)

    MyClass.php
    <?php
    class MyClass {

        public function power($x, $y)
        {
            return pow($x, $y);
        }
    }



    Хочется проверить, правильно ли он работает. О всем классе речь пока не идет — только об одном методе. Напишем для проверки такой тест.

    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'MyClass.php';

    class MyClassTest extends PHPUnit_Framework_TestCase {
        public function testPower()
        {
            $my = new MyClass();
            $this->assertEquals(8, $my->power(2, 3));
     }
    }



    Небольшое отступление о формате тестов.
    • Название класса в тесте складывается из названия тестируемого класса плюс «Test»;
    • Класс для тестирования в большинстве случаев наследуется от PHPUnit_Framework_TestCase;
    • Каждый тест является паблик-методом, название которого начинается с префикса «test»;
    • Внутри теста мы используем один из assert-методов для выяснения соответствует ли результат обработки ожидаемому (подробнее чуть ниже);

    Теперь глядя на тест мы можем понять, что тестируя метод возведения в степень мы создаем экземпляр класса, вызываем нужный нам метод с заранее определенными значениями и проверяем правильно ли были проведены вычисления. Для этой проверки был использован метод assertEquals(), который первым обязательным параметром принимает ожидаемое значение, вторым актуальное и проверяет их соответствие. Размяв мозги и освежив в памяти знания таблицы умножения, мы предположили, что 23=8. На этих данных мы и проверим, как работает наш метод.
    Запускаем тест:

    $ phpunit MyClassTest
    .
    Time: 0 seconds
    OK (1 test, 1 assertion)



    Результат выполнения теста «ОК». Казалось бы можно остановиться на этом, но иногда было бы неплохо для проверки скормить методу не один набор данных. PHPUnit предоставляет нам такую возможность — это провайдеры данных. Провайдер данных тоже является паблик-методом (название не существенно), который возвращает массив наборов данных для каждой итеррации. Для использования провайдера необходимо указать его в теге @dataProvider к тесту.

    Изменим наш тест следующим образом:

    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'MyClass.php';

    class MyClassTest extends PHPUnit_Framework_TestCase {

        /**
        * @dataProvider providerPower
        */

        public function testPower($a, $b, $c)
        {
            $my = new MyClass();
            $this->assertEquals($c, $my->power($a, $b));
        }

        public function providerPower ()
        {
            return array (
                array (2, 2, 4),
                array (2, 3, 9),
                array (3, 5, 243)
            );
        }
    }



    После запуска увидим следующую картину:

    .F.
    Time: 0 seconds
    There was 1 failure:
    1) testPower(MyClassTest) with data set #1 (2, 3, 9)
    Failed asserting that <integer:8> matches expected value <integer:9>.
    /home/user/unit/MyClassTest.php:14
    FAILURES!
    Tests: 3, Assertions: 3, Failures: 1.



    Опишу подробнее. Точка, которую в первом выводе теста многие могли принять за опечатку на самом деле таковой не является — это успешно пройденный тест. F(ailure) — соответственно тест не пройденный. Значит в данном случае, было проведено 3 теста, один из который завершился неудачно. В расширенном описании нам было сказано, какой именно, с каким набором исходных данных, с каким реальным и каким ожидаемым результатом. Если бы 23 действительно равнялось 9-ти, то мы увидели бы таким образом ошибку в нашем сценарии.

    Здесь, как мне кажется, есть смысл отвлечься от несколько абстрактной практики и перейти ко вполне конкретной теории. А именно, описать какими же assert-методами мы располагаем для проверки поведения тестируемых сценариев.

    Два самых простых — это assertFalse() и assertTrue(). Проверяют, является ли полученное значение false и true соответственно. Далее идут уже упомянутый assertEquals() и обратный ему assertNotEquals(). В их использовании есть нюансы. Так при сравнении чисел с плавающей точкой есть возможность указать точность сравнения. Так же эти методы используются для сравнения экземпляров класса DOMDocument, массивов и любых объектов (в последнем случае равенство будет установлено, если атрибуты объектов содержат одинаковые значения). Так же следует упомянуть assertNull() и assertNotNull() которые проверяют соответствие параметра типу данных NULL (да-да, не забываем, что в PHP это отдельный тип данных). Этим возможные сравнения не ограничиваются. Нет смысла в рамках этой статьи заниматься перепечаткой документации, потому приведу по возможности структурированный список всех возможных методов. Более детально интересующиеся могут прочитать здесь

    Базовые методы сравнения
    assertTrue() / assertFalse()
    assertEquals() / assertNotEquals()
    assertGreaterThan()
    assertGreaterThanOrEqual()
    assertLessThan()
    assertLessThanOrEqual()
    assertNull() / assertNotNull()
    assertType() / assertNotType()
    assertSame() / assertNotSame()
    assertRegExp() / assertNotRegExp()

    Методы сравнения массивов
    assertArrayHasKey() / assertArrayNotHasKey()
    assertContains() / assertNotContains()
    assertContainsOnly() / assertNotContainsOnly()

    ООП специфичные методы
    assertClassHasAttribute() / assertClassNotHasAttribute()
    assertClassHasStaticAttribute() / assertClassNotHasStaticAttribute()
    assertAttributeContains() / assertAttributeNotContains()
    assertObjectHasAttribute() / assertObjectNotHasAttribute()
    assertAttributeGreaterThan()
    assertAttributeGreaterThanOrEqual()
    assertAttributeLessThan()
    assertAttributeLessThanOrEqual()

    Методы сравнения файлов
    assertFileEquals() / assertFileNotEquals()
    assertFileExists() / assertFileNotExists()
    assertStringEqualsFile() / assertStringNotEqualsFile()

    Методы сравнения XML
    assertEqualXMLStructure()
    assertXmlFileEqualsXmlFile() / assertXmlFileNotEqualsXmlFile()
    assertXmlStringEqualsXmlFile() / assertXmlStringNotEqualsXmlFile()
    assertXmlStringEqualsXmlString() / assertXmlStringNotEqualsXmlString()

    Разное
    assertTag()
    assertThat()

    Исключения


    Если еще не надоело, то вернемся к практике, а именно к обработке исключений. Для начала модифицируем наш тестируемый класс — введем в него метод, который будет это исключение выбрасывать.

    MyClass.php
    <?php
    class MathException extends Exception {};

    class MyClass {

        // ...

        public function divide($x, $y)
        {
            if (!(boolean)$y)
            {
                throw new MathException('Division by zero');
            }
            return $x / $y;
        }
    }



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

    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'MyClass.php';

    class MyClassTest extends PHPUnit_Framework_TestCase {

        // ...

        /**
        * @expectedException MathException
        */

        public function testDivision1()
        {
            $my = new MyClass();
            $my->divide (8, 0);
        }

        public function testDivision2 ()
        {
            $this->setExpectedException('MathException');
            $my = new MyClass();
            $my->divide(8, 0);
        }
    }



    Тесты, в общем-то, абсолютно идентичны. Выбор способа остается на ваше усмотрение. Помимо механизмов предоставляемых непосредственно PHPUnit, для тестирования исключений можно воспользоваться стандартным try {…} catch (), к примеру, так:

    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'MyClass.php';

    class MyClassTest extends PHPUnit_Framework_TestCase {

        // ...

        public function testDivision3()
        {
            $my = new MyClass();
            try {
                $my->divide (8, 2);
            } catch (MathException $e) {
                return;
            }
            $this->fail ('Not raise an exception');
        }
    }



    В этом примере мы так же видим не рассмотренный ранее способ завершения теста с помощью вызова метода fail(). Вывод теста будет следующим:

    F
    Time: 0 seconds
    There was 1 failure:
    1) testDivision3(MyClassTest)
    Not raise an exception
    /home/user/unit/MyClassTest.php:50



    Принадлежности


    Базовые методы тестирования мы освоили. Можно ли улучшить наш тест? Да. Написанный c начала этой статьи класс проводит несколько тестов, в каждом из которых создается экземпляр тестируемого класса, что абсолютно излишне, потому как PHPUnit предоставляет в наше пользование механизм принадлежностей теста (fixtures). Установить их можно защищенным методом setUp(), который вызывается один раз перед началом каждого теста. После окончания теста вызывается метод tearDown(), в котором мы можем провести «сборку мусора». Таким образом, исправленный тест может выглядеть так:

    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'MyClass.php';
       
    class MyClassTest extends PHPUnit_Framework_TestCase {

        protected $fixture;

        protected function setUp()
        {
            $this->fixture = new MyClass ();
        }

        protected function tearDown()
        {
            $this->fixture = NULL;
        }

        /**
        * @dataProvider providerPower
        */

        public function testPower($a, $b, $c)
        {
            $this->assertEquals($c, $this->fixture->power($a, $b));
        }
       
        public function providerPower()
        {
            return array(
                array(2, 2, 4),
                array(2, 3, 8),
                array(3, 5, 243)
            );
        }

        // …

    }



    Наборы тестов


    После того, как код нескольких классов будет покрыт тестами, становится довольно таки неудобно запускать каждый тест по отдельности. Здесь нам на помощь могут прийти наборы тестов — несколько связанных единой задачей тестов можно объединить в набор и запускать соответственно его. Наборы реализованы классом PHPUnit_Framework_TestSuite. Необходимо создать экземпляр этого класса и добавить в него необходимые тесты с помощью метода addTestSuite(). Так же с помощью метода addTest() возможно добавление другого набора.

    SpecificTests.php
    <?php
    require_once 'PHPUnit/Framework.php';
    // подключаем файлы с тестами
    require_once 'MyClassTest.php';

    class SpecificTests
    {
        public static function suite()
        {
            $suite = new PHPUnit_Framework_TestSuite('MySuite');
            // добавляем тест в набор
            $suite->addTestSuite('MyClassTest');
            return $suite;
        }
    }



    AllTests.php
    <?php
    require_once 'PHPUnit/Framework.php';
    // подключаем файл с набором тестов
    require_once 'SpecificTests.php';

    class AllTests
    {
        public static function suite()
        {
            $suite = new PHPUnit_Framework_TestSuite('AllMySuite');
            // добавляем набор тестов
            $suite->addTest(SpecificTests::suite());
            return $suite;
        }
    }



    А теперь представим себе набор тестов для сценария, работающего с БД. Неужели нам в каждом тесте придется подключаться к базе? Нет — не придется. Для этого можно создать свой класс унаследованный от PHPUnit_Framework_TestSuite, определить его методы setUp() и tearDown() для инициализации интерфейса БД и просто передать его в тест атрибутом sharedFixture. Базы данных мы оставим на потом, а пока попробуем создать собственный набор тестов для уже имеющегося класса.

    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'MyClass.php';

    class MyClassTest extends PHPUnit_Framework_TestCase {
       
        protected $fixture;

        protected function setUp()
        {
            $this->fixture = $this->sharedFixture;
        }

        protected function tearDown()
        {
            $this->fixture = NULL;
        }

        /**
        * @dataProvider providerPower
        */

        public function testPower ($a, $b, $c)
        {
            $this->assertEquals($c, $this->fixture->power($a, $b));
        }

        public function providerPower()
        {
            return array(
                array(2, 2, 4),
                array(2, 3, 8),
                array(3, 5, 243)
            );
        }

        // …

    }



    MySuite.php
    <?php
    require_once 'MyClassTest.php';

    class MySuite extends PHPUnit_Framework_TestSuite {

        protected $sharedFixture;

        public static function suite()
        {
            $suite = new MySuite('MyTests');
            $suite->addTestSuite('MyClassTest');
            return $suite;
        }

        protected function setUp()
        {
            $this->sharedFixture = new MyClass();
        }

        protected function tearDown()
        {
            $this->sharedFixture = NULL;
        }

    }



    Здесь мы в sharedFixture положили экземпляр тестируемого класса, а в тесте просто его использовали — решение не слишком красивое (я бы даже сказал, вообще не красивое), но общее представление о наборах тестов и передаче принадлежностей между тестами оно дает. Если наглядно изобразить очередность вызова методов, то получится нечто вроде такого:

    MySuite::setUp()
    MyClassTest::setUp()
    MyClassTest::testPower()
    MyClassTest::tearDown()
    MyClassTest::setUp()
    MyClassTest::testDivision()
    MyClassTest::tearDown()
    ...
    MySuite::tearDown()



    Дополнительные возможности


    Помимо всего вышеизложенного может возникнуть необходимость проверить не только расчеты и поведение сценария, но так же вывод и скорость отработки. Для этого в наше распоряжение предоставлены расширения PHPUnit_Extensions_OutputTestCase и PHPUnit_Extensions_PerformanceTestCase соответственно. Добавим в наш тестируемый класс еще один метод, и проверим правильно ли он работает.

    MyClass.php
    <?php
    class MyClass {

        // ...

        public function square($x)
        {
            sleep(2);
            print $x * $x;
        }

    }



    MyClassTest.php
    <?php
    require_once 'PHPUnit/Framework.php';
    require_once 'PHPUnit/Extensions/OutputTestCase.php';
    require_once 'PHPUnit/Extensions/PerformanceTestCase.php';
    require_once 'MyClass.php';

    class MyClassOutputTest extends PHPUnit_Extensions_OutputTestCase {

        protected $fixture;

        protected function setUp()
        {
            $this->fixture = $this->sharedFixture;
        }

        protected function tearDown()
        {
            $this->fixture = NULL;
        }
     
        public function testSquare()
        {
            $this->expectOutputString('4');
            $this->fixture->square(2);
        }
    }

    class MyClassPerformanceTest extends PHPUnit_Extensions_PerformanceTestCase {

        protected $fixture;

        protected function setUp()
        {
            $this->fixture = $this->sharedFixture;
        }

        protected function tearDown()
        {
            $this->fixture = NULL;
        }

        public function testPerformance()
        {
            $this->setMaxRunningTime(1);
            $this->fixture->square(4);
        }
    }

    class MyClassTest extends PHPUnit_Framework_TestCase {

     // …

    }



    Задать ожидаемый вывод сценария можно с помощью методов expectOutputString() или expectOutputRegex(). А для метода setMaxRunningTime() планируемое время отработки указывается в секундах. Для того, что бы эти тесты запускались вместе с уже написанными их всего лишь надо добавить к нашему набору:

    MySuite.php
    <?php
    require_once 'MyClassTest.php';

    class MySuite extends PHPUnit_Framework_TestSuite {

        protected $sharedFixture;

        public static function suite()
        {
            $suite = new MySuite('MyTests');
            $suite->addTestSuite('MyClassTest');
            $suite->addTestSuite('MyClassOutputTest');
            $suite->addTestSuite('MyClassPerformanceTest');
            return $suite;
        }

        // ...
       
    }



    Пропускаем тесты


    И напоследок рассмотрим ситуацию, в которой некоторые тесты необходимо пропускать по каким либо причинам. К примеру в том случае, когда на тестируемой машине отсутствует какое-либо расширение PHP, можно убедившись в его отсутствии пометить тест, как пропущенный добавив к его коду следующее:

    if (!extension_loaded('someExtension')) {
        $this->markTestSkipped('Extension is not loaded.');
    }



    Либо в том случае, когда тест написан для кода, которого еще нет в сценарии (не редкая для TDD ситуация) его можно пометить как не реализованный с помощью метода markTestIncomplete()

    Напоследок


    Наверное, на этом пока можно остановиться. Тема модульного тестирования данной статьей далеко не завершена — осталось еще использование mock-объектов, тестирование работы с БД, анализ покрытия кода и многое другое. Но надеюсь, что поставленная цель — ознакомить начинающих с базовыми возможностями PHPUnit и подтолкнуть к использованию юнит-тестов, как одному из средств для достижения большей эффективности — была достигнута.
    Удачи вам, и стабильных приложений.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 90
    • +1
      Спасибо, а продолжение будет?
      • +4
        напишите как тестировать не только простейшие примеры, в которые тестирования и так не надо, а сложные системы, где метод вызивает внутри себ много других, работает с базой, а выводит совсем не инт или булл, а генерирует вывод через шаблон к примеру. ну и принимает пользовательский ввод или зависит от других ранее вызванных методов, а еще от сессии и ее состони
        • +2
          Дело в том, что поводом к написанию данной статьи стал вопрос: «А что есть почитать по PHPUnit на русском?» Начал гуглить — и не нашел. Я прекрасно сознаю, что здесь изложены только основы — но, вы удивитесь, многие не знают и этого. Если тема интересна, то позже попробую описать глубже.
          • –1
            Так порекомундуй «Освой юнит тестирование за 24 часа»
            • +2
              Весьма интересна, на работе как раз столкнулись с проблемой модификации кода, писало более двадцати людей, логика не везде понятна. Вводим юнит тестирование параллельно с рефакторингом. Посему очень хотелось бы почитать про mock объекты
              • 0
                wiki.agiledev.ru/doku.php?id=tdd — пожалуйста =)

                пс: я конечно понимаю что постоянно появляются новые люди, которые всегда будут открывать что-то новое, но хотелось видеть статьи бы не старого нового, а инновационо нового =)
                • 0
                  Спасибо :)

                  ПС благодаря этим новеньким процесс не стоит на месте ;)
              • 0
                а так же как использовать PHPUnit в ZendFramework
              • 0
                Я тут убедился, что даже в книжках, полностью посвещенных TDD, примеры высосаны из пальца (типа:«проверим, что тут 2х2=4»). В итоге, прочитав, так и не понял как это использовать в реальном проекте.
                • 0
                  На «дважды два четыре» просто понять суть, а транслировать на другую задачу уже не должно бы составлять труда.
                • 0
                  Поясните plz про «вывод через шаблон» — что вы имели в виду?
                  • 0
                    ну когда у меня в конце стоит $tpl->display('template.tpl'); и все
                    • 0
                      А что именно нужно проверить?
                      Как значение выглядит на этом самом шаблоне — это отдельная тема. А получаемое значение мы тестируем именно так, как написано в топике.
                  • 0
                    По поводу сложных систем — любая из сложных систем в конечном итоге состоит из минимальных простейших блоков. А тестировать нужно именно самые атомарные элементы алгоритмов.
                    • +1
                      А где остановиться? Получается, тестировать ничего не надо, самые простейшие операции (и, или, не и т.п.) уже давно оттестированы производителем процессора :)
                      • 0
                        Ну обычно это методы классов и интерфейсов. Ну нужно писать отдельно один «Большой Адронный Тест» на всю систему.
                        • 0
                          это уже принципиально другой уровень тестирования — функциональное. оно осуществляется другими инструментами.
                      • +1
                        Метод вызывает в себе другие. Все методы тестируются по-отдельности.

                        Например:
                        1) Есть Method1(), который выполняет некую операцию
                        2) Есть Method2(), который выполняя операцию, использует Method1()
                        3) Вы тестируете поведение Method1() по всем необходимым тест-кейсам (если все тесты OK, вы считаете, что метод работает как нужно)
                        4) Вы тестируете поведение Method2() — тесту все равно, что вы там использовали внутри, лишь бы результат совпадал. Здесь и есть один из ключевых моментов — вы тестируете именно поведение и результат Method2(), а не поведение Method1() в Method2().
                        • +2
                          Вот в этом все равно и заключается главная проблема. А если метод лезет в базу, или открывает соединение по сокету, в качестве сервера… Юнит тестами отлично покрываются методы, которые возращают результат умножения или выводят на экран «Hello world». Или писать огромную кучу моков, заглушек, и непонять еще чего, или позволять Юнит-тесту работать с живой базой или реальными сокетами.
                          • 0
                            Мы в работе использовали подход тестирования поведения (behavior testing) и использовали mock-и. Тесты (иногда дублирующие), которые тестировали состояния базы, соединения и так далее — использовались у нас на стадии интеграционного тестирования (т.е. тестирование не собственно логики, а тестирование именно по сути отправки запросов, соединений и так далее).

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

                            > «Юнит тестами отлично покрываются методы, которые возращают результат умножения или выводят на экран «Hello world».»
                            Юнит-тестами отлично покрываются любые методы, которые возвращают определенный, ожидаемый и измеряемый результат.
                            • 0
                              Делали мы как-то тесты с живой базой. Получилась весьма отвратно — тесты работали медленно и содержали кучу кода: setup базы, cleanup базы — тонны SQL запросов, невозможность параллельного тестирования.

                              Вывод — делать моки. Это позволит не только избавиться от зависимости от БД, но и, например, автоматизировать валидацию поведения при лежащем сервере БД или битых данных (inconsistent data).

                              Хорошая новость в том, что заполучив единожды мок на БД или сокет в следующем проекте его не придется делать с нуля. Тут, безусловно, возможны проблемы с дизайном кода и вопросы типа «как подсунуть мок внутрь логики?» — но это отдельная тема, TDD в помощь.
                              • 0
                                Плохая новость в том, что большинство решения для работы с БД, слабо поддаются мокингу, :) поскольку сами созданы без использование этих принципов. Некоторые современные технологии для работы с БД(ORM), приближают нас к этому. Но пока все еще в стадии развития, хотя самому TDD уже предостаточно времени.
                                Скажем так. TDD выходит из стадии испытания и начинает использоваться в индустрии(asp net mvc полностью построен на нем)
                                • 0
                                  > Плохая новость в том, что большинство решения для работы с БД, слабо поддаются мокингу.

                                  Тю, кто мешает написать враппер вокруг решения для работы с БД и использовать его в проекте? ;-)
                                  • 0
                                    Ни кто не мешает :) Чаще всего так делается. Создаем свой интерфейс, реализуем обертку вокруг какого нить Database и вперед :) Но я же не сказал, что это не возможно :)
                          • 0
                            Мне кажется, этот вопрос немного не по адресу. Тестировать пользовательский ввод, зависимости от других ранее вызванных методов, а еще от сессии и ее состояния — это скорее задача функционального (приемочного) тестирования. Для функционального тестирования веб-приложений есть известный инструмент — Selenium (плагин для ФФ), других лично я не знаю, но, думаю, они существуют и вы их сможете без труда найти.

                            Задача модульного тестирования, как следует из названия — тестировать модули. Модуль — это класс, библиотека функций и т. д. Классическое модульное тестирование — тестирование черного ящика, то есть что там делается внутри метода — не важно (напротив, даже важно именно не знать этого; полагаться на особенности реализации тестируемого модуля — один из неприятных запахов тестового кода). В этой связи также становится неясен вопрос о «сложных системах, где метод вызивает внутри себ много других». Пусть вызывает. Протестируй его.

                            Единственный вопрос, с которым соглашусь — это работа с базой. Тестирование работы с БД — отдельная песня, основной лейт-мотив которой — тестируй на реальной базе. Нет, не на продакшн-базе :) Просто воссоздай в тестовом окружении ровно то, что есть в реальном проекте, только без данных (или с их необходимым минимумом). Все данные, нужные тесту, он должен впихнуть в базу сам, создавая фикстуру.
                          • 0
                            <?php
                            print (int)((0.1 + 0.7) * 10);
                            // думаете 8? проверьте…
                            // или лучше так: «Вы еще не используете BC Math? Тогда мы идем к вам!»

                            Жесть пример. Причем самое интересное что тот же результат выдает и C. Убиться
                            • –2
                              IEEE 754-2008 ответит на вопрос почему
                            • 0
                              Простите если я тупой.
                              А зачем там int?
                              Без int выводит 8
                              • 0
                                без int получается число, которое чуть-чуть меньше 8, но при выводе выводится как 8.
                                Проверить легко: print ((0.1 + 0.7) * 10) — 8;
                            • 0
                              Лично у меня в ветке 3.3 при количестве тестов >700 очень сильно текла память. Копаться в изменениях между 3.2 и 3.3 не стал, поэтому откатился до 3.2.
                              • 0
                                можете рассказать о своем опыте использовании тестов? о том что нужно тестировать обязательно, а что не стоит, сколько в среднем занимает написание одного теста, и как часто тесты помогают отловить ошибки?
                                • 0
                                  Чем больше тестов — тем больше шансов, что сломать все будет сложно. Написание тестов также позволяет найти места, где логика вашего приложения выстроена неправильно. Если вы не можете написать тест на какой-то метод/функция — значит вы неправильно спланировали интерфейсы и внутренние функции.

                                  Можно использовать mockи для параметров функции, чтобы не создавать какие-то сложные объекты.

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

                                  В среднем написание теста занимает минут 10-15 для простых функций.
                              • 0
                                А что есть почитать по PHPUnit на русском?


                                Я нашел довольно хорошо написанный материал по Юнит-тестированию в книге «Профессиональное программирование на PHP» от Джорджа Шлосснейгла.

                                В сети её можно скачать в PDF ;)
                                • 0
                                  всем кому интересно и не читал еще, рекомендую почитать «Профессиональное программирование на PHP (Джордж Шлоснейгл)». Там блочное тестирование освещено достаточно подробно, впрочем как и все остальное.
                                  • 0
                                    Да, отличная книга! Уберегла от множества ошибок в проектировании.
                                  • 0
                                    У вас там парсер-лох. Заменил "->" на &rarr.
                                  • +1
                                    Автор, хотелось бы услышать поподробнее про mock)
                                    • 0
                                      Даа, это целая отдельная тема в TDD — mock-и и разница между тестированием состояния и поведения (про Expectations Testing и все такое)
                                    • 0
                                      доступно написано. Все сам хочу использовать (пишу на cakephp ) но клиенты никак немогут понять почему я буду делать таски дольше и они будут платить больше.
                                      • 0
                                        Так TDD и не должно создавать ситуацию «дольше и платить больше». Это просто альтернативный (я убежден — что более надежный) способ создавать ПО.

                                        P. S. CakePHP + Unit-тестирование = Sehr gut!
                                        • 0
                                          да согласен, но он увеличивает время разработки и уменьшает время на потдержку. Кроме того, бытует мнение что юнит тесты нужно применять только там где они нужны.
                                          В общем при сдоровой конкуренции (фриланс) в основном выигрывает тот, кто сделает быстрее (ну и качественней), но клиент конечно же в код не лезет и понятное дело, что ему чем быстрее, тем лучше. И объясняй ему что такое юнит тесты и что это ему даст.
                                          • 0
                                            Скажу чисто по опыту, никакой теории.

                                            1. Действительно, чтобы стать крутым TDD-разработчиком, нужно время для «въезжания» в тему, но потом продуктивность вырастет. Вы не тратите время на обдумывание кода (все поведение обдумывается на этапе написания тестов, код пишется только, чтобы их выполнить). И что ооочень важно — в большинстве случаев никакого дебага, ибо ошибки ищутся и исправляются в разы быстрее.
                                            2. Объяснять заказчику, что это такое, не нужно ни в коем случае — зачем ему это. Он все равно не понимает, что как разрабатывается.
                                            3. > «Кроме того, бытует мнение что юнит тесты нужно применять только там где они нужны.» В рамках одного проекта — или везде или нигде — частичное покрытие системы тестами — вот где обычно кроется краеугольный камень TDD — все потраченное на тесты время действительно оказывается потраченным зря. Часть модулей протестирована, но общее поведение системы протестировать невозможно.
                                            4. TDD ведет к постоянной интеграции, а это очень круто :)
                                            • +1
                                              юнит тесты не увеличивают качество кода! Они просто позволяют его в будущем дешевле изменять
                                              bishop-it.ru/?p=119

                                              Например, Michael Feathers в своей книге Working Effectively with Legacy Code не призывает покрывать все 100% кода тестами. Мало того, он даже не призывает всегда покрывать тестами код, который вы собираетесь изменять. Он призывает думать. Каждый раз делать осознанный выбор — нужны ли вам в данном месте юнит тесты или нет. Всем советую купить и прочитать эту его книгу. Эта книга — лучшее описание теории и практики написания юнит тестов, что я встречал. Причем именно с точки зрения практики — большая часть книги посвящена описанию стандартных приемов работы со старым кодом. А сам автор имеет огромный практический опыт по улучшению legacy кода.
                                              • 0
                                                Качество кода зависит от разработчика прежде всего. Улучшение legacy-кода и переход уже существующего легаси-проекта на TDD и CI — это отдельная большая тема, но методы существуют.

                                                Про покрытие — я, разумеется, условно рассматривал. 100%-го покрытия не бывает на практике. Здесь нужно чувствовать определенную грань. Не нужно плодить лишние и дублирующие тест-кейсы.

                                                «Каждый раз делать осознанный выбор — нужны ли вам в данном месте юнит тесты или нет.»
                                                Это верно, но могу точно сказать, что «пара тестов» на всяк случай не сделают погоды.
                                              • 0
                                                Вы не тратите время на обдумывание кода (все поведение обдумывается на этапе написания тестов, код пишется только, чтобы их выполнить)


                                                Есть путь менее правильный, но более быстрый — тестирование осуществляется вручную с консоли, а потом результаты просто копи-пастятся в доктест. Это не даёт гибкости в том плане, что удобны лишь проверки на полное равенство вывода эталонному, но зато накладных расходов на написание таких тестов нет. Поясню примером:

                                                Захотелось мне написать функцию для разложения числа на простые множители. Создаю файлик my.py со следующим кодом:

                                                def f(x):
                                                        assert x > 1
                                                        i = 2
                                                        while i <= x:
                                                                if x % i == 0:
                                                                        x /= i
                                                                        yield i
                                                                else:
                                                                        i += 1
                                                


                                                Как проще и быстрее тестировать такое? Правильно, в консоли. Запускаю с тестовыми параметрами и смотрю результат:

                                                >>> from my import f
                                                >>> list(f(1))
                                                Traceback (most recent call last):
                                                  File "<stdin>", line 1, in <module>
                                                  File "my.py", line 8, in f
                                                    assert x > 1
                                                AssertionError
                                                >>> list(f(3))
                                                [3]
                                                >>> list(f(4))
                                                [2, 2]
                                                >>> list(f(60))
                                                [2, 2, 3, 5]
                                                


                                                Визуально понимаю, что результат меня устраивает. Использую буфер обмена для помещения вывода в код как теста:

                                                def f(x):
                                                        '''
                                                        >>> list(f(1))
                                                        Traceback (most recent call last):
                                                          File "<stdin>", line 1, in <module>
                                                          File "my.py", line 8, in f
                                                            assert x > 1
                                                        AssertionError
                                                        >>> list(f(3))
                                                        [3]
                                                        >>> list(f(4))
                                                        [2, 2]
                                                        >>> list(f(60))
                                                        [2, 2, 3, 5]
                                                        >>>
                                                        '''
                                                        assert x > 1
                                                        i = 2
                                                        while i <= x:
                                                                if x % i == 0:
                                                                        x /= i
                                                                        yield i
                                                                else:
                                                                        i += 1
                                                


                                                Теперь как-то надо уметь этот тест запускать. Простейший способ — добавить в конец файла немножко кода для вызова теста текущего модуля при загрузке этого модуля первым:

                                                if __name__ == '__main__':
                                                        import doctest
                                                        doctest.testmod()
                                                


                                                Запускаем:
                                                $ python my.py
                                                $


                                                Вывод пустой — всё в порядке.

                                                Сэмулируем теперь ошибку логики, поправив тест:
                                                $ python my.py
                                                *******************
                                                File "my.py", line 13, in __main__.f
                                                Failed example:
                                                    list(f(60))
                                                Expected:
                                                    [2, 2, 3, 6]
                                                Got:
                                                    [2, 2, 3, 5]
                                                *******************
                                                1 items had failures:
                                                   1 of   4 in __main__.f
                                                ***Test Failed*** 1 failures.
                                                


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

                                                Итого — тесты имеются, а накладных расходов — минимум.

                                                В рамках одного проекта — или везде или нигде


                                                Не соглашусь. В рамках большого проекта начать писать тесты на всё сразу — не хватит сил, не писать по-прежнему — неразумно, потому тесты пишутся на новый, изменившийся функционал и на обнаруженные ошибки. Через некоторое время можно не переживать о том, что значительная часть кода не покрыта тестами — значит, этот код относительно стабилен.
                                                • 0
                                                  «Визуально понимаю, что результат меня устраивает.»
                                                  Ну так вы, по сути, тест и написали. Только одно из преимуществ тестов — запустить их все сразу, провести постоянную интеграцию. Не каждый же раз вручную запускать каждый кусочек, который нужно проверить. Впрочем, может я что-то не так понял?

                                                  «В рамках большого проекта начать писать тесты на всё сразу — не хватит сил, не писать по-прежнему — неразумно, потому тесты пишутся на новый, изменившийся функционал и на обнаруженные ошибки. Через некоторое время можно не переживать о том, что значительная часть кода не покрыта тестами — значит, этот код относительно стабилен.»
                                                  Естественно. На все сразу и смысла нет. То, что вы написали — это нормальная стратегия внедрения TDD в проект legacy-системы. Именно так это и делается.

                                                  А насчет того, как и где проще всего тестировать — как по мне, так прямо в IDE, что называется «не отходя от кассы». Мы для этой цели с Visual Studio использовали TestDriven.NET
                                                  Для других платформ и технологий тоже есть что-то подобное (для Java точно).
                                                  • 0
                                                    Не каждый же раз вручную запускать каждый кусочек, который нужно проверить. Впрочем, может я что-то не так понял?


                                                    Угу, это в примере было проще запустить тестирование именно так. А вообще доктесты (а именно так называются тесты из примера) прекрасно интегрируются в классические юнит-тесты. Сказал про минус доктестов, но забыл сказать про главный плюс — они являются ещё и документацией к функции, потому так и называются.

                                                    проще всего тестировать — как по мне, так прямо в IDE


                                                    С трудом понимаю… В смысле в IDE есть интерфейс для запуска конкретного теста, группы тестов? Или для создания тестов? Опишите, пожалуйста.
                                              • +2
                                                TDD — для команд, для постоянной разработки, когда задача решается не в течении недели двух и благополучно забывается.

                                                Уммм, представил себе идеальный мир, где все используют TDD и Agile.

                                                К тебе обращаются как к разработчику-фрилансеру и просят решить задачу в рамках проекта.
                                                Ты подключаешься к svn(или любой другой системе контроля версии), получаешь последнею версию, просмотрел набор тестов(По названию тестов, быстро понял где что проверяется :)).
                                                Написал тесты для своей задачи, реализовал их, немного по-рефакторил, запустил всю сборку на тестирование.
                                                Сделал commit, где то там у заказчика сработала CI, все успешно собралось и протестировалось.
                                                Пошел забрал свои деньги в банке :)
                                                • 0
                                                  Не только для команд. Один человек вполне может это использовать, как и команда.
                                                  Просто для команд это куда более критично, когда над одним и тем же кодом работает больше одного человека.
                                                  • 0
                                                    Западники уже реализуют подобные механизмы. Collaborative Development Environments. У нас пока это все только из области фантастики. Хотя технически — вообще все легко.
                                                    • 0
                                                      Ну это больше организационный вопрос, нежели технический — когда будут понимающие проджект-манагеры, они введут эти штуки в рабочий режим.
                                            • 0
                                              Что именно проверять в тесте, что мокать, что не мокать, как поступать с приватными методами, как быть при работе с БД. Нет смысла(просто не получится) раскрывать все эти вопросы в одной статье, которая тем более привязана к какой-то технологии(языку программирования). Надо рассматривать их отдельно и пробовать, пробовать, пробовать. И Бек и Файлер пишут, и не только они, что любая команды которая начинает использовать TDD(и unit тестирование в рамках TDD), в среднем тратят по пол года, чтобы хоть что-то понять и наработать свои практики.
                                              • 0
                                                Макс, ну для нас это вообще больная тема :)
                                              • 0
                                                Подскажите какой-нибудь опенсорсный пхп-проект, где это все хорошо реализовано.
                                                • 0
                                                  этот проект называется PHPUnit, в каталоге Tests дистрибутива все и увидите
                                                  • 0
                                                    Я думаю, товарищ ТМС имел в виду проект, сделанный с использованием TDD и PHPUnit, где в исходных кодах можно найти не только сам код, но и наборы тестов.
                                                    • 0
                                                      Точно так.
                                                      • +1
                                                        Код проекта PHPUnit тестируется с помощью PHPUnit :)
                                                  • +1
                                                    Вроде Limb PHP framework полностью покрыт тестами
                                                    При использовании Симфонии тоже настоятельно рекомендуют в документации.
                                                    • 0
                                                      Действительно. Уже что-то, спасибо. :-)
                                                      • 0
                                                        «полностью»
                                                        мало того, что абсолютно 100% покрытие невозможно, так это ещё и считается плохой практикой — писать тестов больше, чем нужно.
                                                      • 0
                                                        mzz
                                                        • 0
                                                          ага, хороший пример использования simpletest
                                                      • –5
                                                        Может, для entrprise-level языков такая парадигма разработки и приемлема, но для ПХП — это просто выглядит кривлянием.
                                                        Время на написание этих тестов несоизмеримо больше, чем может понадобиться на гипотетический отлов сложно улавливаемых ошибок при внесении очередного изменения.

                                                        • 0
                                                          Все зависит от сложности разрабатываемой системы.
                                                          • 0
                                                            Причем тут язык? Даже для Javascript можно писать unit-тесты.
                                                            • +1
                                                              время на написание тестов несоизмеримо меньше времени, которое вы тратите на то же самое, но ручное тестирование в процессе написания кода.
                                                              • 0
                                                                хе, занятно: у комментария выше нет времени написания.
                                                                • 0
                                                                  занятно: у комментария выше нет времени написания :-)
                                                                  • –3
                                                                    да ты что?
                                                                    А тесты кто тестировать будет?
                                                                    • 0
                                                                      Тесты крайне прямолинейны и вероятность ошибки в них куда ниже, чем вероятность ошибиться в коде, держа все в голове во время написания.
                                                                • +1
                                                                  >Поскольку PHPUnit уже стал стандартом де-факто в тестировании PHP приложений, то на нем и остановимся.
                                                                  с чего принято такое решение?
                                                                  репозиторий жив, баги фиксяться, пользоваться проще (имхо), гуя нормальная есть, нет зависимости от пакетов PEAR

                                                                  по теме:
                                                                  forum.agiledev.ru/index.php?t=msg&&th=1173&goto=7201#msg_7201
                                                                  simpletest.svn.sourceforge.net/viewvc/simpletest/
                                                                  • +1
                                                                    Объясните, пожалуйста, кто разбирается.

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

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

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

                                                                    На такое пишут тесты? Где бы про это узнать, а то элементарных примеров много, но не понятно, как это масштабируется.
                                                                    • +1
                                                                      У вас есть задача разобрать CSV файл и поместить данные в БД. По сути любая задача разбивается на много маленьких.
                                                                      Например в контексте этой задачи есть такая:
                                                                      «Прочитать строку из файла, распарсить по правилам и разместить в массиве нужные значение из строки.»
                                                                      Я пишу тест на входе метода строка на выходе массив и проверяю массив(actual и expected).
                                                                      Далее с помощью тестов я описываю те ситуации которые должен обрабатывать мой метод. Например, если пропущено значение в строке или формат строки не соответствует ожиданиям. И так далее и тому подобное.
                                                                      По сути тесты это проверка ситуаций, из которых состоит например модуль, который берет данные из файла и помещает из в БД.
                                                                      • +1
                                                                        Мне видится две проблемы:

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

                                                                        2. Тестами можно проверить формальные вещи. Но если я захочу более интеллектуальный контроль, то придется думать над алгоритмами тестов. И здесь уже я о некоторых вариантах могу даже не догадываться. Скажем, захочу проверить безопасность веб-формы. Это ведь нужно очень разноплановые тесты писать. Ну и опять же это небыстрое дело.
                                                                    • 0
                                                                      Тесты покроют всю основную функциональность, их будет ровно столько сколько ситуаций обрабатывает твой код. Естественно если ты меняешь поведения кода, надо будет менять тесты. Вернее ты в начале подправишь тесты, а потом код. И еще при правильном написании тестов, если не меняется поведение метода, то не меняются и сами тесты.
                                                                      Из практики.
                                                                      Был большой метод который добавлял route, но в процессе добавления надо было проверить его уникальность, и это тоже было в методе(был большой, плохой метод :), там вообще было много не нужного).
                                                                      Был написан тест(их там было несколько) который проверял наличия исключения при не уникальности маршрута(именно это и должен был делать наш метод).
                                                                      Тестов которые бы проверяли работу по проверки уникальности не было(так как метод этого не должен делать)
                                                                      В результате в момент рефакторинга все ненужные куски кода из метода, растащили по другим классам и методам, сама объявленная логика метода не поменялась, тесты так же не пришлось трогать.
                                                                      А все потому что они были написаны правильно

                                                                      Тесты должны быть простыми. Есть такое правильно. Если тест непонятен и требует слишком много, значит сам метод надо делать. он делает не свою работу. Тут тесты выступают в качестве инструмента, для построения архитектуры. На agiledev.ru есть очень много хороших статьей. Но опять повторюсь, надо пробовать и пробовать.
                                                                      • 0
                                                                        пользуюсь simple-tests, но теперь из статьи понимаю что надо наверно переходить на phpUnit. Например как в сипл-тестах реализовать конструкцию

                                                                        … public function testPower($a, $b, $c)
                                                                        {
                                                                        $this->assertEquals($c, $this->fixture->power($a, $b));
                                                                        }
                                                                        public function providerPower()…

                                                                        как я понимаю, метод providerPower реализует перебор нескольких вариантов вызова testPower. В симпл-тестах подобное приходится разрабатывать «на коленке». Или может я что-то упускаю?
                                                                        • НЛО прилетело и опубликовало эту надпись здесь
                                                                          • 0
                                                                            Спасибо за статью.
                                                                            Именно то что я хотел прочитать, но до поиска руки не доходили; и о чудо оно само нашлось.
                                                                            +1
                                                                            • 0
                                                                              Отличная статья. но есть один вопрос.
                                                                              Понимаю, что примеры взяты из мануала к PHPUnit, но всё-таки можете объяснить почему

                                                                              • 0
                                                                                Для создания иерархии TestSuite-ов используется непонятный class SpecificTests и public static function suite(), вместо того, чтобы просто делать class SpecificTests extends PHPUnit_Framework_TestSuite и прописывать всё у него в конструкторе или в setUp()?

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