Сегодня я хотел бы затронуть тему очень важную для каждого разработчика. Автоматизированное тестирование разработанного приложения. Использование юнит-тестирование позволяет автоматически распознавать ошибки в разработанных и отлаженных частях исходного кода, при внесении новых правок, изменении архитектуры приложения.
Подробнее о юнит-тестировании можно почитать на викапедии, а мы сегодня поговорим о применимости phpUnit к тестированию модулей написанных для Magento.
Источники информации
- Официальный сайт phpUnit – http://www.phpunit.de/
- Примеры использования – http://habrahabr.ru/blogs/php/56289/ и http://habrahabr.ru/blogs/php/89175
- PhpUnit + NetBeans http://habrahabr.ru/blogs/php/70046/
- PhpUnit + Magento – http://www.magentocommerce.com/wiki/development/phpunit_integration_with_magento
Предварительные условия
Для создания юнит-тестов Вам необходимо произвести установку PhpUnit и xDebug. Использование xDebug позволяет производить генерацию отчетов по тестированию, с отображением информации об покрытие исходного кода тестами. Я не буду подробно останаливаться на процессе установки. В интернете достаточно много примеров установки и настройки. Например вот этот краткий мануал
Настройка phpUnit для Magento
Мы подошли к основной части статьи. Для тестирования приложений мы будем использовать комбинацию phpunit.xml (конфигурационный файл), bootstrap.php (стартовый файл для инициализации) и ControllerTestCase.php (базовый класс для всех юнит тестов).
Для тестирования будем использовать слейдующую структуру каталогов.

Папка с тестами находится в корне установленной Magento, на одном уровне с папками app, skin, media и т.д.
Каталог unit – содержит файлы юнит-тестов, каталог report – сгенерированный отчет выполнения тестирования.
Переходим к содержимому файлов.
phpunit.xml – файл конфигурации
<phpunit bootstrap="bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="true" syntaxCheck="true"> <testsuite name="nameofsuite"> <directory>./</directory> </testsuite> <filter> <blacklist> <directory suffix=".php">../tests</directory> <directory suffix=".php">../lib</directory> <directory suffix=".php">../app/code/core</directory> <directory suffix=".php">../app/code/community</directory> </blacklist> </filter> <logging> <log type="coverage-html" target="./report" charset="UTF-8" yui="true" highlight="true" lowUpperBound="35" highLowerBound="70"/> </logging> </phpunit>
В данном файле мы указываем какие дирректори не надо включать в тестирование. К ним я отнес файлы тестов, файлы community библиотек, и исходные файлы magento. Так же мы добавляем возможность автоматической генерации отчета по выполнению тестирования.
bootstrap.php – стартовый файл
require_once 'PHPUnit/Framework.php'; require_once './../app/Mage.php'; Mage::app('default');
Достаточно небольшой файл производящий подключение классов для юнит-тестирования и инициализирующий magento приложение (подобный подход для иницализации так же используется в magento cron.php)
ControllerTestCase.php – базовый класс наследник для всех юнит тестов
abstract class ControllerTestCase extends PHPUnit_Framework_TestCase { public $connection; public function setUp() { parent::setUp(); $class = $this->_colorize(get_class($this), 'blue'); $method = $this->_colorize($this->getName()); echo "\n" . $class . ' -> ' . $method; $this->_time = microtime(1); $this->connection = Mage::getModel('Core/Mysql4_Config')->getReadConnection(); $this->connection->beginTransaction(); } public function tearDown() { $time = sprintf('%0.3f sec', microtime(1) - $this->_time); echo "\t" . $this->getCount() . '(Assertions)'; echo $this->_colorize("\t" . $time, 'green'); $this->connection->rollBack(); parent::tearDown(); } private function _colorize($text, $color = 'yellow') { switch ($color) { case 'red': $color = "1;31m"; break; case 'green': $color = "1;32m"; break; case 'blue': $color = "1;34m"; break; default: $color = "1;33m"; break; } return "\033[" . $color . $text . "\033[m"; } }
Содержимое файла частично было взято из юнит тестов CMF разработанной на базе Zend Framework – ZFCore.
Функция setUp выполняется перед каждым юнит-тестом. Она получает ссылку на коннект к базе данных и стартует транзацию (нет смысла каждый раз производить чистку magento приложения от тестовых данных). Производит запись времени начала теста и вывод дополнительной полезной информации.
Функция tearDown выполняется после каждого юнит-теста. Производит откат транзации и выводит тестовую информацию о времени выполнения теста и количестве выполненных утверждений.
unit/SimpleFileOfTest.php – базовый каркас для каждого созданного юнит теста
class SimpleFileOfTest extends ControllerTestCase { public function setUp() { parent::setUp(); } public function testMagentoModel() { // Some test case } public function tearDown() { parent::tearDown(); } }
В данном файле мы производим вызов методов перед началом теста и после окончания, не забывая вызвать методы родительского класса. Метод testMagentoModel будет выполняться как тестовый кейс.
Такой структуру приложения будет достаточно для выполнения тестирования Ваших magento модулей.
Пример юнит-тестирования
Ранее я описывал возможность создания order в Magento программными средствами. Давайте создим юнит тест для этого функционала.
Создаем файл CreateOrderTest.php с слейдующим содержанием.
class CreateOrdersTest extends ControllerTestCase { protected $createOrderModel; public function setUp() { parent::setUp(); $this->createOrderModel = new Spiri7_Test_Model_CreateOrders; } /** * Test order creation for some predeffined Values * * @dataProvider orderProvider */ public function testCreateOrder($productInfo, $billingInfo, $shippingInfo) { $result = $this->createOrderModel->createOrder($productInfo, $billingInfo, $shippingInfo); $orderId = $result['orderId']; // Test for right Create Order. Order Id Must me not zero if ($orderId == 0) { $this->fail($result['message']); } // Checking create order for right product $order = Mage::getModel('sales/order')->load($orderId); $items = $order->getAllItems(); $itemcount = count($items); // We adding only single product $this->assertTrue($itemcount == 1); $name=array(); $unitPrice=array(); $sku=array(); $ids=array(); $qty=array(); foreach ($items as $itemId => $item) { $unitPrice[]=$item->getPrice(); $ids[]=$item->getProductId(); $qtyOrdered[]=$item->getQtyOrdered();//ToInvoice(); } // Order created for right product $this->assertEquals($ids[0], $productInfo[0]['id']); // Price for product same as we set $this->assertEquals($unitPrice[0], $productInfo[0]['price']); // Qty of ordered product same as we adding $this->assertEquals($qtyOrdered[0], $productInfo[0]['qty']); } public function orderProvider() { // Billing address used for order $orderBilling = array(); $orderBilling['firstname'] = "Alex"; $orderBilling['lastname'] = "Filin"; $orderBilling['company'] = ''; $orderBilling['email'] = 'alexandr.otchenashev@gmail.com'; $orderBilling['street'] = array(); $orderBilling['street'][0] = "Address part Line1"; $orderBilling['street'][1] = "Address part Line2"; $orderBilling['city'] = "Dnepropetrovsk"; $orderBilling['region_id'] = 1; $orderBilling['region'] = ""; $orderBilling['postcode'] = "49100"; $orderBilling['country_id'] = "US"; $orderBilling['telephone'] = "0238559322"; $orderBilling['fax'] = "0838559111"; // Order Shipping information $orderShipping = array(); $orderShipping['title'] = "UPS"; $orderShipping['price'] = 100.00; $dataArray = array( "Simple Product" => array( array( // Product Info 0 => array( 'id' => 47, // Simple Product ID without custom options 'qty'=> 3, 'price' => 123 ) ), $orderBilling, $orderShipping, ), "Simple Product With Custom Options" => array( array( // Product Info 0 => array( 'id' => 48, // Simple product ID with custom options 'qty'=> 3, 'price' => 123 ) ), $orderBilling, $orderShipping, ) ); return $dataArray; } public function tearDown() { parent::tearDown(); }
В setUp производим загрузку модели для создания ордеров. Метод orderProvider возвращает набор данных используемых для тестирования (я произвожу тестирования для simpleproduct с произвольными опциями и без). Метод testCreateOrder производит тестирование создания ордера для наших наборов данных. Обртите внимание что после вызова метода создания, мы проверяем на наличие в возврате id созданного заказа. Если id = 0, тест завершается неудачей и выводом ошибки. При успешном создании производится сверка на правильность добавленного продукта, правильной установки цены и количества товара.
Производим запуск тестов. Для этого в дирректории /tests/ выполняем команду phpunit, результат выполнения будет слейдующий:

Как видно из результата, второй тест завершился неудачей. Возвратилась ошибка.
В результате выполнения тестов был сгенерирован report с информацией о покрытии кода.

Выводы
Unit-тестирование выполяемое для magento не сильно отличается от тестирования любого другого приложение. Единственным недостатком следует признать сложность начальной конфигурации, и проблемотичность отладки итоговых тестов. Так же для меня остается не раскрытым вопрос о тестировании контроллеров.
Спасибо за внимание. Буду раз вашим комментариям и уточнением. А как производите тестирование magento модулей, Вы?
