使用Atoum测试

除了PHPUnit和Codeception,Atoum是一个简单的单元测试框架。你可以使用这个框架,用于测试你的扩展,或者测试你应用的代码。

准备

为新的项目创建一个空文件夹。

如何做…

在这个小节中,我们将会创建一个演示,使用Atoum测试购物车扩展。

准备扩展框架

  1. 首先,为你的扩展创建目录结构:
  1. book
  2. └── cart
  3. ├── src
  4. └── tests
  1. 作为一个composer包使用扩展。准备book/cart/composer.json文件:
  1. {
  2. "name": "book/cart",
  3. "type": "yii2-extension",
  4. "require": {
  5. "yiisoft/yii2": "~2.0"
  6. },
  7. "require-dev": {
  8. "atoum/atoum": "^2.7"
  9. },
  10. "autoload": {
  11. "psr-4": {
  12. "book\\cart\\": "src/",
  13. "book\\cart\\tests\\": "tests/"
  14. }
  15. },
  16. "extra": {
  17. "asset-installer-paths": {
  18. "npm-asset-library": "vendor/npm",
  19. "bower-asset-library": "vendor/bower"
  20. }
  21. }
  22. }
  1. 添加如下内容到book/cart/.gitignore文件:
  1. /vendor
  2. /composer.lock
  1. 安装扩展所有的依赖:
  1. composer install
  1. 现在我们将会得到如下结构:
  1. book
  2. └── cart
  3. ├── src
  4. ├── tests
  5. ├── .gitignore
  6. ├── composer.json
  7. ├── phpunit.xml.dist
  8. └── vendor

写扩展代码

使用PHPUnit作单元测试小节复制CartStorageInterfaceSessionStorage类。

最后,我们可以得到如下结构:

  1. book
  2. └── cart
  3. ├── src
  4. ├── storage
  5. ├── SessionStorage.php
  6. └── StorageInterface.php
  7. └── Cart.php
  8. ├── tests
  9. ├── .gitignore
  10. ├── composer.json
  11. └── vendor

写扩展测试

  1. 添加book/cart/tests/bootstrap.php入口脚本:
  1. <?php
  2. defined('YII_DEBUG') or define('YII_DEBUG', true);
  3. defined('YII_ENV') or define('YII_ENV', 'test');
  4. require(__DIR__ . '/../vendor/autoload.php');
  5. require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
  1. 在每一个测试前,通过初始化Yii应用创建一个测试基类,然后在销毁它:
  1. <?php
  2. namespace book\cart\tests;
  3. use yii\di\Container;
  4. use yii\console\Application;
  5. use mageekguy\atoum\test;
  6. abstract class TestCase extends test
  7. {
  8. public function beforeTestMethod($method)
  9. {
  10. parent::beforeTestMethod($method);
  11. $this->mockApplication();
  12. }
  13. public function afterTestMethod($method)
  14. {
  15. $this->destroyApplication();
  16. parent::afterTestMethod($method);
  17. }
  18. protected function mockApplication()
  19. {
  20. new Application([
  21. 'id' => 'testapp',
  22. 'basePath' => __DIR__,
  23. 'vendorPath' => dirname(__DIR__) . '/vendor',
  24. 'components' => [
  25. 'session' => [
  26. 'class' => 'yii\web\Session',
  27. ],
  28. ]
  29. ]);
  30. }
  31. protected function destroyApplication()
  32. {
  33. \Yii::$app = null;
  34. \Yii::$container = new Container();
  35. }
  36. }
  1. 添加一个基于内存的干净的fake类,并继承StorageInterface接口:
  1. <?php
  2. namespace book\cart\tests;
  3. use book\cart\storage\StorageInterface;
  4. class FakeStorage implements StorageInterface
  5. {
  6. private $items = [];
  7. public function load()
  8. {
  9. return $this->items;
  10. }
  11. public function save(array $items)
  12. {
  13. $this->items = $items;
  14. }
  15. }

它会存储条目到一个私有变量中,而不是使用真正的session。它允许我们独立运行测试(不适用真正的存储驱动),并提升测试性能。

  1. 添加Cart测试类:
  1. <?php
  2. namespace book\cart\tests\units;
  3. use book\cart\tests\FakeStorage;
  4. use book\cart\Cart as TestedCart;
  5. use book\cart\tests\TestCase;
  6. class Cart extends TestCase
  7. {
  8. /**
  9. * @var TestedCart
  10. */
  11. private $cart;
  12. public function beforeTestMethod($method)
  13. {
  14. parent::beforeTestMethod($method);
  15. $this->cart = new TestedCart(['storage' => new
  16. FakeStorage()]);
  17. }
  18. public function testEmpty()
  19. {
  20. $this->array($this->cart->getItems())->isEqualTo([]);
  21. $this->integer($this->cart->getCount())->isEqualTo(0);
  22. $this->integer($this->cart->getAmount())->isEqualTo(0);
  23. }
  24. public function testAdd()
  25. {
  26. $this->cart->add(5, 3);
  27. $this->array($this->cart->getItems())->isEqualTo([5 =>
  28. 3]);
  29. $this->cart->add(7, 14);
  30. $this->array($this->cart->getItems())->isEqualTo([5 =>
  31. 3, 7 => 14]);
  32. $this->cart->add(5, 10);
  33. $this->array($this->cart->getItems())->isEqualTo([5 =>
  34. 13, 7 => 14]);
  35. }
  36. public function testSet()
  37. {
  38. $this->cart->add(5, 3);
  39. $this->cart->add(7, 14);
  40. $this->cart->set(5, 12);
  41. $this->array($this->cart->getItems())->isEqualTo([5 =>
  42. 12, 7 => 14]);
  43. }
  44. public function testRemove()
  45. {
  46. $this->cart->add(5, 3);
  47. $this->cart->remove(5);
  48. $this->array($this->cart->getItems())->isEqualTo([]);
  49. }
  50. public function testClear()
  51. {
  52. $this->cart->add(5, 3);
  53. $this->cart->add(7, 14);
  54. $this->cart->clear();
  55. $this->array($this->cart->getItems())->isEqualTo([]);
  56. }
  57. public function testCount()
  58. {
  59. $this->cart->add(5, 3);
  60. $this->integer($this->cart->getCount())->isEqualTo(1);
  61. $this->cart->add(7, 14);
  62. $this->integer($this->cart->getCount())->isEqualTo(2);
  63. }
  64. public function testAmount()
  65. {
  66. $this->cart->add(5, 3);
  67. $this->integer($this->cart->getAmount())->isEqualTo(3);
  68. $this->cart->add(7, 14);
  69. $this->integer($this->cart->getAmount())->isEqualTo(17);
  70. }
  71. public function testEmptyStorage()
  72. {
  73. $cart = new TestedCart();
  74. $this->exception(function () use ($cart) {
  75. $cart->getItems();
  76. })->hasMessage('Storage must be set');
  77. }
  78. }
  1. 添加一个独立的测试,用于检查SessionStorage类:
  1. <?php
  2. namespace book\cart\tests\units\storage;
  3. use book\cart\storage\SessionStorage as TestedStorage;
  4. use book\cart\tests\TestCase;
  5. class SessionStorage extends TestCase
  6. {
  7. /**
  8. * @var TestedStorage
  9. */
  10. private $storage;
  11. public function beforeTestMethod($method)
  12. {
  13. parent::beforeTestMethod($method);
  14. $this->storage = new TestedStorage(['key' => 'test']);
  15. }
  16. public function testEmpty()
  17. {
  18. $this
  19. ->given($storage = $this->storage)
  20. ->then
  21. ->array($storage->load())
  22. ->isEqualTo([]);
  23. }
  24. public function testStore()
  25. {
  26. $this
  27. ->given($storage = $this->storage)
  28. ->and($storage->save($items = [1 => 5, 6 => 12]))
  29. ->then
  30. ->array($this->storage->load())
  31. ->isEqualTo($items)
  32. ;
  33. }
  34. }
  1. 现在我们将会得到如下结构:
  1. book
  2. └── cart
  3. ├── src
  4. ├── storage
  5. ├── SessionStorage.php
  6. └── StorageInterface.php
  7. └── Cart.php
  8. ├── tests
  9. ├── units
  10. ├── storage
  11. └── SessionStorage.php
  12. └── Cart.php
  13. ├── bootstrap.php
  14. ├── FakeStorage.php
  15. └── TestCase.php
  16. ├── .gitignore
  17. ├── composer.json
  18. └── vendor

运行测试

在使用composer install命令安装所有的依赖期间,Composer包管理器安装Atoum包到vendor目录中,并将可执行文件atoum放在vendor/bin子文件夹中。

现在我们可以运行如下脚本:

  1. cd book/cart
  2. vendor/bin/atoum -d tests/units -bf tests/bootstrap.php

此外,我们可以看到如下测试报告:

  1. > atoum path: /book/cart/vendor/atoum/atoum/vendor/bin/atoum
  2. > atoum version: 2.7.0
  3. > atoum path: /book/cart/vendor/atoum/atoum/vendor/bin/atoum
  4. > atoum version: 2.7.0
  5. > PHP path: /usr/bin/php5
  6. > PHP version:
  7. => PHP 5.5.9-1ubuntu4.16 (cli)
  8. > book\cart\tests\units\Cart...
  9. [SSSSSSSS__________________________________________________][8/8]
  10. => Test duration: 1.13 seconds.
  11. => Memory usage: 3.75 Mb.
  12. > book\cart\tests\units\storage\SessionStorage...
  13. [SS________________________________________________________][2/2]
  14. => Test duration: 0.03 second.
  15. => Memory usage: 1.00 Mb.
  16. > Total tests duration: 1.15 seconds.
  17. > Total tests memory usage: 4.75 Mb.
  18. > Code coverage value: 16.16%

每一个S符号表示一次成功的测试。

尝试通过注释unset操作故意破坏cart:

  1. class Cart extends Component
  2. {
  3. ...
  4. public function remove($id)
  5. {
  6. $this->loadItems();
  7. if (isset($this->_items[$id])) {
  8. // unset($this->_items[$id]);
  9. }
  10. $this->saveItems();
  11. }
  12. ...
  13. }

再次运行测试:

  1. > atoum version: 2.7.0
  2. > PHP path: /usr/bin/php5
  3. > PHP version:
  4. => PHP 5.5.9-1ubuntu4.16 (cli)
  5. book\cart\tests\units\Cart...
  6. [SSFSSSSS__________________________________________________][8/8]
  7. => Test duration: 1.09 seconds.
  8. => Memory usage: 3.25 Mb.
  9. > book\cart\tests\units\storage\SessionStorage...
  10. [SS________________________________________________________][2/2]
  11. => Test duration: 0.02 second.
  12. => Memory usage: 1.00 Mb.
  13. ...
  14. Failure (2 tests, 10/10 methods, 0 void method, 0 skipped method, 0
  15. uncompleted method, 1 failure, 0 error, 0 exception)!
  16. > There is 1 failure:
  17. => book\cart\tests\units\Cart::testRemove():
  18. In file /book/cart/tests/units/Cart.php on line 53, mageekguy\atoum\
  19. asserters\phpArray() failed: array(1) is not equal to array(0)
  20. -Expected
  21. +Actual
  22. @@ -1 +1,3 @@
  23. -array(0) {
  24. +array(1) {
  25. + [5] =>
  26. + int(3)

在这个例子中,我们看到一次错误(用F表示),以及一个错误报告。

分析代码覆盖率

你必须安装XDebug PHP扩展,https://xdebug.org。例如,在Ubuntu或者Debian上,你可以在终端中输入如下命令:

  1. sudo apt-get install php5-xdebug

在Windows上,你需要打开php.ini文件,并添加自定义代码路径到你的PHP安装目录下:

  1. [xdebug]
  2. zend_extension_ts=C:/php/ext/php_xdebug.dll

或者,如果你使用非线程安全的版本,输入如下:

  1. [xdebug]
  2. zend_extension=C:/php/ext/php_xdebug.dll

安装过XDebug以后,创建book/cart/coverage.php配置文件,并添加覆盖率报告选项:

  1. <?php
  2. use \mageekguy\atoum;
  3. /** @var atoum\scripts\runner $script */
  4. $report = $script->addDefaultReport();
  5. $coverageField = new atoum\report\fields\runner\coverage\
  6. html('Cart', __DIR__ . '/tests/coverage');
  7. $report->addField($coverageField);

现在使用-c选项来使用这个配置再次运行测试:

  1. vendor/bin/atoum -d tests/units -bf tests/bootstrap.php -c coverage.php

在运行这个测试以后,在浏览器中打开tests/coverage/index.html。你将会看到每一个目录和类的一个明确的覆盖率报告:

使用Atoum测试 - 图1

你可以点击任何类,并分析代码的哪些行在测试过程中还没有被执行。

工作原理…

Atoum测试框架支持行为驱动设计(BDD)语法流,如下:

  1. public function testSome()
  2. {
  3. $this
  4. ->given($cart = new TestedCart())
  5. ->and($cart->add(5, 13))
  6. ->then
  7. ->sizeof($cart->getItems())
  8. ->isEqualTo(1)
  9. ->array($cart->getItems())
  10. ->isEqualTo([5 => 3])
  11. ->integer($cart->getCount())
  12. ->isEqualTo(1)
  13. ->integer($cart->getAmount())
  14. ->isEqualTo(3);
  15. }

但是,你可以使用常用的类PHPUnit语法来写单元测试:

  1. public function testSome()
  2. {
  3. $cart = new TestedCart();
  4. $cart->add(5, 3);
  5. $this->array($cart->getItems())->isEqualTo([5 => 3])
  6. ->integer($cart->getCount())->isEqualTo(1)
  7. ->integer($cart->getAmount())->isEqualTo(3);
  8. }

Atoum也支持代码覆盖率报告,用于分析测试质量。

参考