PHPUnit is the most popular PHP testing framework. It is simple for configuration and usage. Also, the framework supports code coverage reports and has a lot of additional plugins. Codeception from the previous recipe uses PHPUnit for own work and writing unit tests. In this recipe, we will create a demonstration shopping cart extension with PHPUnit tests.
Create a new yii2-app-basic
application using the Composer package manager, as described in the official guide at http://www.yiiframework.com/doc-2.0/guidestart-installation.html.
First, we must create a new empty directory for own extension.
book └── cart ├── src └── tests
To work with the extension as a Composer package, prepare the book/cart/composer.json
file like this:
{ "name": "book/cart", "type": "yii2-extension", "require": { "yiisoft/yii2": "~2.0" }, "require-dev": { "phpunit/phpunit": "4.*" }, "autoload": { "psr-4": { "book\cart\": "src/", "book\cart\tests\": "tests/" } }, "extra": { "asset-installer-paths": { "npm-asset-library": "vendor/npm", "bower-asset-library": "vendor/bower" } } }
book/cart/.gitignore
file with the following lines:/vendor /composer.lock
book/cart/phpunit.xml.dist
like this:<?xml version="1.0" encoding="utf-8"?> <phpunit bootstrap="./tests/bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="false"> <testsuites> <testsuite name="Test Suite"> <directory>./tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">./src/</directory> </whitelist> </filter> </phpunit>
composer install
book └── cart ├── src ├── tests ├── .gitignore ├── composer.json ├── phpunit.xml.dist └── vendor
To write the extension code, follow these steps:
bookcartCart
class in the src
directory:<?php namespace bookcart; use bookcartstorageStorageInterface; use yiiaseComponent; use yiiaseInvalidConfigException; class Cart extends Component { /** * @var StorageInterface */ private $_storage; /** * @var array */ private $_items; public function setStorage($storage) { if (is_array($storage)) { $this->_storage = Yii::createObject($storage); } else { $this->_storage = $storage; } } public function add($id, $amount = 1) { $this->loadItems(); if (isset($this->_items[$id])) { $this->_items[$id] += $amount; } else { $this->_items[$id] = $amount; } $this->saveItems(); } public function set($id, $amount) { $this->loadItems(); $this->_items[$id] = $amount; $this->saveItems(); } public function remove($id) { $this->loadItems(); if (isset($this->_items[$id])) { unset($this->_items[$id]); } $this->saveItems(); } public function clear() { $this->loadItems(); $this->_items = []; $this->saveItems(); } public function getItems() { $this->loadItems(); return $this->_items; } public function getCount() { $this->loadItems(); return count($this->_items); } public function getAmount() { $this->loadItems(); return array_sum($this->_items); } private function loadItems() { if ($this->_storage === null) { throw new InvalidConfigException('Storage must be set'); } if ($this->_items === null) { $this->_items = $this->_storage->load(); } } private function saveItems() { $this->_storage->save($this->_items); } }
StorageInterface
interface in the src/storage
subdirectory:<?php namespace bookcartstorage; interface StorageInterface { /** * @return array */ public function load(); /** * @param array $items */ public function save(array $items); }
and SessionStorage class:
namespace bookcartstorage; use Yii; class SessionStorage implements StorageInterface { public $sessionKey = 'cart'; public function load() { return Yii::$app->session->get($this->sessionKey, []); } public function save(array $items) { Yii::$app->session->set($this->sessionKey, $items); } }
book └── cart ├── src │ ├── storage │ │ ├── SessionStorage.php │ │ └── StorageInterface.php │ └── Cart.php ├── tests ├── .gitignore ├── composer.json ├── phpunit.xml.dist └── vendor
To conduct the extension test, follow these steps:
book/cart/tests/bootstrap.php
entry script for PHPUnit:<?php defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_ENV') or define('YII_ENV', 'test'); require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
<?php namespace bookcart ests; use yiidiContainer; use yiiwebApplication; abstract class TestCase extends PHPUnit_Framework_TestCase { protected function setUp() { parent::setUp(); $this->mockApplication(); } protected function tearDown() { $this->destroyApplication(); parent::tearDown(); } protected function mockApplication() { new Application([ 'id' => 'testapp', 'basePath' => __DIR__, 'vendorPath' => dirname(__DIR__) . '/vendor', ]); } protected function destroyApplication() { Yii::$app = null; Yii::$container = new Container(); } }
StorageInterface
interface:<?php namespace bookcart estsstorage; use bookcartstorageStorageInterface; class FakeStorage implements StorageInterface { private $items = []; public function load() { return $this->items; } public function save(array $items) { $this->items = $items; } }
It will store items into a private variable instead of working with a real session. It allows to run tests independently (without real storage driver) and also improves testing performance.
CartTest
class:<?php namespace bookcart ests; use bookcartCart; use bookcart estsstorageFakeStorage; class CartTest extends TestCase { /** * @var Cart */ private $cart; public function setUp() { parent::setUp(); $this->cart = new Cart(['storage' => new FakeStorage()]); } public function testEmpty() { $this->assertEquals([], $this->cart->getItems()); $this->assertEquals(0, $this->cart->getCount()); $this->assertEquals(0, $this->cart->getAmount()); } public function testAdd() { $this->cart->add(5, 3); $this->assertEquals([5 => 3], $this->cart->getItems()); $this->cart->add(7, 14); $this->assertEquals([5 => 3, 7 => 14], $this->cart->getItems()); $this->cart->add(5, 10); $this->assertEquals([5 => 13, 7 => 14], $this->cart->getItems()); } public function testSet() { $this->cart->add(5, 3); $this->cart->add(7, 14); $this->cart->set(5, 12); $this->assertEquals([5 => 12, 7 => 14], $this->cart->getItems()); } public function testRemove() { $this->cart->add(5, 3); $this->cart->remove(5); $this->assertEquals([], $this->cart->getItems()); } public function testClear() { $this->cart->add(5, 3); $this->cart->add(7, 14); $this->cart->clear(); $this->assertEquals([], $this->cart->getItems()); } public function testCount() { $this->cart->add(5, 3); $this->assertEquals(1, $this->cart->getCount()); $this->cart->add(7, 14); $this->assertEquals(2, $this->cart->getCount()); } public function testAmount() { $this->cart->add(5, 3); $this->assertEquals(3, $this->cart->getAmount()); $this->cart->add(7, 14); $this->assertEquals(17, $this->cart->getAmount()); } public function testEmptyStorage() { $cart = new Cart(); $this->setExpectedException('yiiaseInvalidConfigException'); $cart->getItems(); } }
SessionStorage
class:<?php namespace bookcart estsstorage; use bookcartstorageSessionStorage; use bookcart estsTestCase; class SessionStorageTest extends TestCase { /** * @var SessionStorage */ private $storage; public function setUp() { parent::setUp(); $this->storage = new SessionStorage(['key' => 'test']); } public function testEmpty() { $this->assertEquals([], $this->storage->load()); } public function testStore() { $this->storage->save($items = [1 => 5, 6 => 12]); $this->assertEquals($items, $this->storage->load()); } }
book └── cart ├── src │ ├── storage │ │ ├── SessionStorage.php │ │ └── StorageInterface.php │ └── Cart.php ├── tests │ ├── storage │ │ ├── FakeStorage.php │ │ └── SessionStorageTest.php │ ├── bootstrap.php │ ├── CartTest.php │ └── TestCase.php ├── .gitignore ├── composer.json ├── phpunit.xml.dist └── vendor
During the installation of all dependencies with the composer install
command, the Composer package manager installs the PHPUnit
package into the vendor
directory and places the executable file phpunit
in the vendor/bin
subdirectory.
Now we can run the following script:
cd book/cart vendor/bin/phpunit
We must see the following testing report:
PHPUnit 4.8.26 by Sebastian Bergmann and contributors. .......... Time: 906 ms, Memory: 11.50MB OK (10 tests, 16 assertions)
Each dot shows a success result of the correspondent test.
Try to deliberately break an own cart by commenting the unset
operation:
class Cart extends Component { … public function remove($id) { $this->loadItems(); if (isset($this->_items[$id])) { // unset($this->_items[$id]); } $this->saveItems(); } ... }
Run the tests again:
PHPUnit 4.8.26 by Sebastian Bergmann and contributors. ...F...... Time: 862 ms, Memory: 11.75MB There was 1 failure: 1) bookcart estsCartTest::testRemove Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( + 5 => 3 ) /book/cart/tests/CartTest.php:52 FAILURES! Tests: 10, Assertions: 16, Failures: 1
In this case, we have seen one failure (marked as F
instead of dot) and a failure report.
You must install the XDebug PHP extension from https://xdebug.org. For example, on Ubuntu or Debian, you can type the following in your terminal:
sudo apt-get install php5-xdebug
On Windows, you must open the php.ini
file and add the custom code with path to your PHP installation directory:
[xdebug] zend_extension_ts=C:/php/ext/php_xdebug.dll
Alternatively, if you use the non-thread safe edition, type the following:
[xdebug] zend_extension=C:/php/ext/php_xdebug.dll
After installing XDebug, run the tests again with the --coverage-html
flag and specify a report directory:
vendor/bin/phpunit --coverage-html tests/_output
After running open the tests/_output/index.html
file in your browser, you will see an explicit coverage report for each directory and class:
You can click on any class and analyze which lines of code have not been executed during the testing process. For example, open our Cart
class report:
In our case, we forgot to test the creating storage from array configuration.
After publishing the extension on Packagist, we can install a one-to-any project:
composer require book/cart
Also, enable the component in the application configuration file:
'components' => [ // … 'cart' => [ 'class' => 'bookcartCart', 'storage' => [ 'class' => 'bookcartstorageSessionStorage', ], ], ],
As an alternative way without publishing the extension on Packagist, we must set up the @book
alias for enabling correct class autoloading:
$config = [ 'id' => 'basic', 'basePath' => dirname(__DIR__), 'bootstrap' => ['log'], 'aliases' => [ '@book' => dirname(__DIR__) . '/book', ], 'components' => [ 'cart' => [ 'class' => 'bookcartCart', 'storage' => [ 'class' => 'bookcartstorageSessionStorage', ], ], // ... ], ]
Anyway, we can use it as the Yii::$app->cart
component in our project:
Yii::$app->cart->add($product->id, $amount);
Before creating your own tests, you must just create any subdirectory and add the phpunit.xml
or phpunit.xml.dist
file in the root directory of your project:
<?xml version="1.0" encoding="utf-8"?> <phpunit bootstrap="./tests/bootstrap.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="false"> <testsuites> <testsuite name="Test Suite"> <directory>./tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">./src/</directory> </whitelist> </filter> </phpunit>
PHPUnit loads configuration from the second file if the first one does not exist in the working directory. Also, you can create the bootstrap.php
file by initializing autoloader and your framework's environments:
<?php defined('YII_DEBUG') or define('YII_DEBUG', true); defined('YII_ENV') or define('YII_ENV', 'test'); require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
Finally, you can install PHPUnit via Composer (locally or globally) and use the phpunit
console command in the directory with the XML configuration file.
PHPUnit scans the testing directory and finds files with the *Test.php
suffix. All your test classes must extend the PHPUnit_Framework_TestCase
class and contain public methods with the test*
prefix like this:
class MyTest extends TestCase { public function testSomeFunction() { $this->assertTrue(true); } }
In the body of your tests, you can use any of the existing assert*
methods:
$this->assertEqual('Alex', $model->name); $this->assertTrue($model->validate()); $this->assertFalse($model->save()); $this->assertCount(3, $items); $this->assertArrayHasKey('username', $model->getErrors()); $this->assertNotNull($model->author); $this->assertInstanceOf('appmodelsUser', $model->author);
Also, you can override the setUp()
or tearDown()
methods for adding expressions that will be run before and after each test method.
For example, you can define own base TestCase
class by reinitializing the Yii application:
<?php namespace bookcart ests; use yiidiContainer; use yiiwebApplication; abstract class TestCase extends PHPUnit_Framework_TestCase { protected function setUp() { parent::setUp(); $this->mockApplication(); } protected function tearDown() { $this->destroyApplication(); parent::tearDown(); } protected function mockApplication() { new Application([ 'id' => 'testapp', 'basePath' => __DIR__, 'vendorPath' => dirname(__DIR__) . '/vendor', ]); } protected function destroyApplication() { Yii::$app = null; Yii::$container = new Container(); } }
Now you can extend this class in your subclasses. Even your test
method will work with an own instance of the application. It helps to avoid side effects and to create independent tests.
3.147.54.6