使用Behat作单元测试

Behat是一个BDD框架,以人类可读的语句测试你的代码,这些语句以多个使用例子描述你的代码行为。

准备

为一个新的项目创建一个空的目录。

如何做…

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

准备扩展结构

  1. 首先,为你的扩展创建一个目录结构:
  1. book
  2. └── cart
  3. ├── src
  4. └── features
  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. "phpunit/phpunit": "4.*",
  9. "behat/behat": "^3.1"
  10. },
  11. "autoload": {
  12. "psr-4": {
  13. "book\\cart\\": "src/",
  14. "book\\cart\\features\\": "features/"
  15. }
  16. },
  17. "extra": {
  18. "asset-installer-paths": {
  19. "npm-asset-library": "vendor/npm",
  20. "bower-asset-library": "vendor/bower"
  21. }
  22. }
  23. }
  1. 添加如下内容到book/cart/.gitignore
  1. /vendor
  2. /composer.lock
  1. 安装扩展所有的依赖:
  1. composer install
  1. 现在我们得到如下结构:
  1. book
  2. └── cart
  3. ├── src
  4. ├── features
  5. ├── .gitignore
  6. ├── composer.json
  7. └── vendor

写扩展代码

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

最后,我们得到如下结构。

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

写扩展测试

  1. 添加book/cart/features/bootstrap/bootstrap.php入口脚本:
  1. <?php
  2. defined('YII_DEBUG') or define('YII_DEBUG', true);
  3. defined('YII_ENV') or define('YII_ENV', 'test');
  4. require_once __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
  1. 创建features/cart.feature文件,并写cart测试场景:
  1. Feature: Shopping cart
  2. In order to buy products
  3. As a customer
  4. I need to be able to put interesting products into a cart
  5. Scenario: Checking empty cart
  6. Given there is a clean cart
  7. Then I should have 0 products
  8. Then I should have 0 product
  9. And the overall cart amount should be 0
  10. Scenario: Adding products to the cart
  11. Given there is a clean cart
  12. When I add 3 pieces of 5 product
  13. Then I should have 3 pieces of 5 product
  14. And I should have 1 product
  15. And the overall cart amount should be 3
  16. When I add 14 pieces of 7 product
  17. Then I should have 3 pieces of 5 product
  18. And I should have 14 pieces of 7 product
  19. And I should have 2 products
  20. And the overall cart amount should be 17
  21. When I add 10 pieces of 5 product
  22. Then I should have 13 pieces of 5 product
  23. And I should have 14 pieces of 7 product
  24. And I should have 2 products
  25. And the overall cart amount should be 27
  26. Scenario: Change product count in the cart
  27. Given there is a cart with 5 pieces of 7 product
  28. When I set 3 pieces for 7 product
  29. Then I should have 3 pieces of 7 product
  30. Scenario: Remove products from the cart
  31. Given there is a cart with 5 pieces of 7 product
  32. When I add 14 pieces of 7 product
  33. And I clear cart
  34. Then I should have empty cart
  1. 添加features/storage.feature存储测试文件:
  1. Feature: Shopping cart storage
  2. I need to be able to put items into a storage
  3. Scenario: Checking empty storage
  4. Given there is a clean storage
  5. Then I should have empty storage
  6. Scenario: Save items into storage
  7. Given there is a clean storage
  8. When I save 3 pieces of 7 product to the storage
  9. Then I should have 3 pieces of 7 product in the storage
  1. features/bootstrap/CartContext.php文件中,为所有的步骤添加实现:
  1. <?php
  2. use Behat\Behat\Context\SnippetAcceptingContext;
  3. use book\cart\Cart;
  4. use book\cart\features\bootstrap\storage\FakeStorage;
  5. use yii\di\Container;
  6. use yii\web\Application;
  7. require_once __DIR__ . '/bootstrap.php';
  8. class CartContext implements SnippetAcceptingContext
  9. {
  10. /**
  11. * @var Cart
  12. * */
  13. private $cart;
  14. /**
  15. * @Given there is a clean cart
  16. */
  17. public function thereIsACleanCart()
  18. {
  19. $this->resetCart();
  20. }
  21. /**
  22. * @Given there is a cart with :pieces of :product product
  23. */
  24. public function thereIsAWhichCostsPs($product, $amount)
  25. {
  26. $this->resetCart();
  27. $this->cart->set($product, floatval($amount));
  28. }
  29. /**
  30. * @When I add :pieces of :product
  31. */
  32. public function iAddTheToTheCart($product, $pieces)
  33. {
  34. $this->cart->add($product, $pieces);
  35. }
  36. /**
  37. * @When I set :pieces for :arg2 product
  38. */
  39. public function iSetPiecesForProduct($pieces, $product)
  40. {
  41. $this->cart->set($product, $pieces);
  42. }
  43. /**
  44. * @When I clear cart
  45. */
  46. public function iClearCart()
  47. {
  48. $this->cart->clear();
  49. }
  50. /**
  51. * @Then I should have empty cart
  52. */
  53. public function iShouldHaveEmptyCart()
  54. {
  55. PHPUnit_Framework_Assert::assertEquals(
  56. 0,
  57. $this->cart->getCount()
  58. );
  59. }
  60. /**
  61. * @Then I should have :count product(s)
  62. */
  63. public function iShouldHaveProductInTheCart($count)
  64. {
  65. PHPUnit_Framework_Assert::assertEquals(
  66. intval($count),
  67. $this->cart->getCount()
  68. );
  69. }
  70. /**
  71. * @Then the overall cart amount should be :amount
  72. */
  73. public function theOverallCartPriceShouldBePs($amount)
  74. {
  75. PHPUnit_Framework_Assert::assertSame(
  76. intval($amount),
  77. $this->cart->getAmount()
  78. );
  79. }
  80. /**
  81. * @Then I should have :pieces of :product
  82. */
  83. public function iShouldHavePiecesOfProduct($pieces,
  84. $product)
  85. {
  86. PHPUnit_Framework_Assert::assertArraySubset(
  87. [intval($product) => intval($pieces)],
  88. $this->cart->getItems()
  89. );
  90. }
  91. private function resetCart()
  92. {
  93. $this->cart = new Cart(['storage' => new
  94. FakeStorage()]);
  95. }
  96. }
  1. 此外,在features/bootstrap/StorageContext.php文件中,添加如下内容:
  1. <?php
  2. use Behat\Behat\Context\SnippetAcceptingContext;
  3. use book\cart\Cart;
  4. use book\cart\features\bootstrap\storage\FakeStorage;
  5. use book\cart\storage\SessionStorage;
  6. use yii\di\Container;
  7. use yii\web\Application;
  8. require_once __DIR__ . '/bootstrap.php';
  9. class StorageContext implements SnippetAcceptingContext
  10. {
  11. /**
  12. * @var SessionStorage
  13. * */
  14. private $storage;
  15. /**
  16. * @Given there is a clean storage
  17. */
  18. public function thereIsACleanStorage()
  19. {
  20. $this->mockApplication();
  21. $this->storage = new SessionStorage(['key' => 'test']);
  22. }
  23. /**
  24. * @When I save :pieces of :product to the storage
  25. */
  26. public function iSavePiecesOfProductToTheStorage($pieces,
  27. $product)
  28. {
  29. $this->storage->save([$product => $pieces]);
  30. }
  31. /**
  32. * @Then I should have empty storage
  33. */
  34. public function iShouldHaveEmptyStorage()
  35. {
  36. PHPUnit_Framework_Assert::assertCount(
  37. 0,
  38. $this->storage->load()
  39. );
  40. }
  41. /**
  42. * @Then I should have :pieces of :product in the storage
  43. */
  44. public function
  45. iShouldHavePiecesOfProductInTheStorage($pieces, $product)
  46. {
  47. PHPUnit_Framework_Assert::assertArraySubset(
  48. [intval($product) => intval($pieces)],
  49. $this->storage->load()
  50. );
  51. }
  52. private function mockApplication()
  53. {
  54. Yii::$container = new Container();
  55. new Application([
  56. 'id' => 'testapp',
  57. 'basePath' => __DIR__,
  58. 'vendorPath' => __DIR__ . '/../../vendor',
  59. ]);
  60. }
  61. }
  1. 添加features/bootstrap/CartContext/FakeStorage.php文件,这是一个fake存储类:
  1. <?php
  2. namespace book\cart\features\bootstrap\storage;
  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. }
  1. 添加book/cart/behat.yml
  1. default:
  2. suites:
  3. default:
  4. contexts:
  5. - CartContext
  6. - StorageContext
  1. 现在我们将得到如下结构:
  1. book
  2. └── cart
  3. ├── src
  4. ├── storage
  5. ├── SessionStorage.php
  6. └── StorageInterface.php
  7. └── Cart.php
  8. ├── features
  9. ├── bootstrap
  10. ├── storage
  11. └── FakeStorage.php
  12. ├── bootstrap.php
  13. ├── CartContext.php
  14. └── StorageContext.php
  15. ├── cart.feature
  16. └── storage.feature
  17. ├── .gitignore
  18. ├── behat.yml
  19. ├── composer.json
  20. └── vendor

现在我们运行我们的测试。

运行测试

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

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

  1. cd book/cart
  2. vendor/bin/behat

此外,我们将会看到如下测试报告:

  1. Feature: Shopping cart
  2. In order to buy products
  3. As a customer
  4. I need to be able to put interesting products into a cart
  5. Scenario: Checking empty cart # features/cart.feature:6
  6. Given there is a clean cart # thereIsACleanCart()
  7. Then I should have 0 products #
  8. iShouldHaveProductInTheCart()
  9. Then I should have 0 product #
  10. iShouldHaveProductInTheCart()
  11. And the overall cart amount should be 0 #
  12. theOverallCartPriceShouldBePs()
  13. ...
  14. Feature: Shopping cart storage
  15. I need to be able to put items into a storage
  16. Scenario: Checking empty storage # features/storage.feature:4
  17. Given there is a clean storage # thereIsACleanStorage()
  18. Then I should have empty storage # iShouldHaveEmptyStorage()
  19. ...
  20. 6 scenarios (6 passed)
  21. 31 steps (31 passed)
  22. 0m0.23s (13.76Mb)

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

  1. class Cart extends Component
  2. {
  3. public function set($id, $amount)
  4. {
  5. $this->loadItems();
  6. // $this->_items[$id] = $amount;
  7. $this->saveItems();
  8. }
  9. ...
  10. }

现在再次运行测试:

  1. Feature: Shopping cart
  2. In order to buy products
  3. As a customer
  4. Feature: Shopping cart
  5. In order to buy products
  6. As a customer
  7. I need to be able to put interesting products into a cart
  8. ...
  9. Scenario: Change product count in the cart # features/
  10. cart.feature:31
  11. Given there is a cart with 5 pieces of 7 prod #
  12. thereIsAWhichCostsPs()
  13. When I set 3 pieces for 7 product #
  14. iSetPiecesForProduct()
  15. Then I should have 3 pieces of 7 product #
  16. iShouldHavePiecesOf()
  17. Failed asserting that an array has the subset Array &0 (
  18. 7 => 3
  19. ).
  20. Scenario: Remove products from the cart # features/
  21. cart.feature:36
  22. Given there is a cart with 5 pieces of 7 prod #
  23. thereIsAWhichCostsPs()
  24. When I add 14 pieces of 7 product #
  25. iAddTheToTheCart()
  26. And I clear cart # iClearCart()
  27. Then I should have empty cart #
  28. iShouldHaveEmptyCart()
  29. --- Failed scenarios:
  30. features/cart.feature:31
  31. 6 scenarios (5 passed, 1 failed)
  32. 31 steps (30 passed, 1 failed)
  33. 0m0.22s (13.85Mb)

在这个例子中,我们看到了一次失败和一次失败报告。

工作原理…

Behat是一个BDD测试框架。它促进writing preceding human-readable testing scenarios to low-level technical implementation。

当我们为每一个特性写场景时,我们可以使用操作的一个集合:

  1. Scenario: Adding products to the cart
  2. Given there is a clean cart
  3. When I add 3 pieces of 5 product
  4. Then I should have 3 pieces of 5 product
  5. And I should have 1 product
  6. And the overall cart amount should be 3

Behat解析我们的句子,并找到相关的实现:

  1. class FeatureContext implements SnippetAcceptingContext
  2. {
  3. /**
  4. * @When I add :pieces of :product
  5. */
  6. public function iAddTheToTheCart($product, $pieces)
  7. {
  8. $this->cart->add($product, $pieces);
  9. }
  10. }

你可以创建一个单FeatureContext类(默认),或者为特性集合场景创建指定的上下文的集合。

参考

欲了解更多关于Behat的信息,参考如下URL:

欲了解更多关于其它测试框架的信息,参考本章中的其它小节。