使用Behat作单元测试
Behat是一个BDD框架,以人类可读的语句测试你的代码,这些语句以多个使用例子描述你的代码行为。
准备
为一个新的项目创建一个空的目录。
如何做...
在这个小节中,我们将会创建一个演示,使用Behat测试购物车扩展。
准备扩展结构
- 首先,为你的扩展创建一个目录结构:
book
└── cart
├── src
└── features
- 作为一个Composer包使用这个扩展,准备
book/cart/composer.json
文件如下:
{
"name": "book/cart",
"type": "yii2-extension",
"require": {
"yiisoft/yii2": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "4.*",
"behat/behat": "^3.1"
},
"autoload": {
"psr-4": {
"book\\cart\\": "src/",
"book\\cart\\features\\": "features/"
}
},
"extra": {
"asset-installer-paths": {
"npm-asset-library": "vendor/npm",
"bower-asset-library": "vendor/bower"
}
}
}
- 添加如下内容到
book/cart/.gitignore
:
/vendor
/composer.lock
- 安装扩展所有的依赖:
composer install
- 现在我们得到如下结构:
book
└── cart
├── src
├── features
├── .gitignore
├── composer.json
└── vendor
写扩展代码
从使用PHPUnit做单元测试小节复制Cart
、StorageInterface
和SessionStorage
类。
最后,我们得到如下结构。
book
└── cart
├── src
│ ├── storage
│ │ ├── SessionStorage.php
│ │ └── StorageInterface.php
│ └── Cart.php
├── features
├── .gitignore
├── composer.json
└── vendor
写扩展测试
- 添加
book/cart/features/bootstrap/bootstrap.php
入口脚本:
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
require_once __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
- 创建
features/cart.feature
文件,并写cart测试场景:
Feature: Shopping cart
In order to buy products
As a customer
I need to be able to put interesting products into a cart
Scenario: Checking empty cart
Given there is a clean cart
Then I should have 0 products
Then I should have 0 product
And the overall cart amount should be 0
Scenario: Adding products to the cart
Given there is a clean cart
When I add 3 pieces of 5 product
Then I should have 3 pieces of 5 product
And I should have 1 product
And the overall cart amount should be 3
When I add 14 pieces of 7 product
Then I should have 3 pieces of 5 product
And I should have 14 pieces of 7 product
And I should have 2 products
And the overall cart amount should be 17
When I add 10 pieces of 5 product
Then I should have 13 pieces of 5 product
And I should have 14 pieces of 7 product
And I should have 2 products
And the overall cart amount should be 27
Scenario: Change product count in the cart
Given there is a cart with 5 pieces of 7 product
When I set 3 pieces for 7 product
Then I should have 3 pieces of 7 product
Scenario: Remove products from the cart
Given there is a cart with 5 pieces of 7 product
When I add 14 pieces of 7 product
And I clear cart
Then I should have empty cart
- 添加
features/storage.feature
存储测试文件:
Feature: Shopping cart storage
I need to be able to put items into a storage
Scenario: Checking empty storage
Given there is a clean storage
Then I should have empty storage
Scenario: Save items into storage
Given there is a clean storage
When I save 3 pieces of 7 product to the storage
Then I should have 3 pieces of 7 product in the storage
- 在
features/bootstrap/CartContext.php
文件中,为所有的步骤添加实现:
<?php
use Behat\Behat\Context\SnippetAcceptingContext;
use book\cart\Cart;
use book\cart\features\bootstrap\storage\FakeStorage;
use yii\di\Container;
use yii\web\Application;
require_once __DIR__ . '/bootstrap.php';
class CartContext implements SnippetAcceptingContext
{
/**
* @var Cart
* */
private $cart;
/**
* @Given there is a clean cart
*/
public function thereIsACleanCart()
{
$this->resetCart();
}
/**
* @Given there is a cart with :pieces of :product product
*/
public function thereIsAWhichCostsPs($product, $amount)
{
$this->resetCart();
$this->cart->set($product, floatval($amount));
}
/**
* @When I add :pieces of :product
*/
public function iAddTheToTheCart($product, $pieces)
{
$this->cart->add($product, $pieces);
}
/**
* @When I set :pieces for :arg2 product
*/
public function iSetPiecesForProduct($pieces, $product)
{
$this->cart->set($product, $pieces);
}
/**
* @When I clear cart
*/
public function iClearCart()
{
$this->cart->clear();
}
/**
* @Then I should have empty cart
*/
public function iShouldHaveEmptyCart()
{
PHPUnit_Framework_Assert::assertEquals(
0,
$this->cart->getCount()
);
}
/**
* @Then I should have :count product(s)
*/
public function iShouldHaveProductInTheCart($count)
{
PHPUnit_Framework_Assert::assertEquals(
intval($count),
$this->cart->getCount()
);
}
/**
* @Then the overall cart amount should be :amount
*/
public function theOverallCartPriceShouldBePs($amount)
{
PHPUnit_Framework_Assert::assertSame(
intval($amount),
$this->cart->getAmount()
);
}
/**
* @Then I should have :pieces of :product
*/
public function iShouldHavePiecesOfProduct($pieces,
$product)
{
PHPUnit_Framework_Assert::assertArraySubset(
[intval($product) => intval($pieces)],
$this->cart->getItems()
);
}
private function resetCart()
{
$this->cart = new Cart(['storage' => new
FakeStorage()]);
}
}
- 此外,在
features/bootstrap/StorageContext.php
文件中,添加如下内容:
<?php
use Behat\Behat\Context\SnippetAcceptingContext;
use book\cart\Cart;
use book\cart\features\bootstrap\storage\FakeStorage;
use book\cart\storage\SessionStorage;
use yii\di\Container;
use yii\web\Application;
require_once __DIR__ . '/bootstrap.php';
class StorageContext implements SnippetAcceptingContext
{
/**
* @var SessionStorage
* */
private $storage;
/**
* @Given there is a clean storage
*/
public function thereIsACleanStorage()
{
$this->mockApplication();
$this->storage = new SessionStorage(['key' => 'test']);
}
/**
* @When I save :pieces of :product to the storage
*/
public function iSavePiecesOfProductToTheStorage($pieces,
$product)
{
$this->storage->save([$product => $pieces]);
}
/**
* @Then I should have empty storage
*/
public function iShouldHaveEmptyStorage()
{
PHPUnit_Framework_Assert::assertCount(
0,
$this->storage->load()
);
}
/**
* @Then I should have :pieces of :product in the storage
*/
public function
iShouldHavePiecesOfProductInTheStorage($pieces, $product)
{
PHPUnit_Framework_Assert::assertArraySubset(
[intval($product) => intval($pieces)],
$this->storage->load()
);
}
private function mockApplication()
{
Yii::$container = new Container();
new Application([
'id' => 'testapp',
'basePath' => __DIR__,
'vendorPath' => __DIR__ . '/../../vendor',
]);
}
}
- 添加
features/bootstrap/CartContext/FakeStorage.php
文件,这是一个fake存储类:
<?php
namespace book\cart\features\bootstrap\storage;
use book\cart\storage\StorageInterface;
class FakeStorage implements StorageInterface
{
private $items = [];
public function load()
{
return $this->items;
}
public function save(array $items)
{
$this->items = $items;
}
}
- 添加
book/cart/behat.yml
:
default:
suites:
default:
contexts:
- CartContext
- StorageContext
- 现在我们将得到如下结构:
book
└── cart
├── src
│ ├── storage
│ │ ├── SessionStorage.php
│ │ └── StorageInterface.php
│ └── Cart.php
├── features
│ ├── bootstrap
│ │ ├── storage
│ │ │ └── FakeStorage.php
│ │ ├── bootstrap.php
│ │ ├── CartContext.php
│ │ └── StorageContext.php
│ ├── cart.feature
│ └── storage.feature
├── .gitignore
├── behat.yml
├── composer.json
└── vendor
现在我们运行我们的测试。
运行测试
在使用composer install
命令安装所有依赖期间,Composer包管理器安装Behat包到vendor
目录中,并将可执行文件behat
放到vendor/bin
子文件夹中。
现在我们可以运行如下脚本:
cd book/cart
vendor/bin/behat
此外,我们将会看到如下测试报告:
Feature: Shopping cart
In order to buy products
As a customer
I need to be able to put interesting products into a cart
Scenario: Checking empty cart # features/cart.feature:6
Given there is a clean cart # thereIsACleanCart()
Then I should have 0 products #
iShouldHaveProductInTheCart()
Then I should have 0 product #
iShouldHaveProductInTheCart()
And the overall cart amount should be 0 #
theOverallCartPriceShouldBePs()
...
Feature: Shopping cart storage
I need to be able to put items into a storage
Scenario: Checking empty storage # features/storage.feature:4
Given there is a clean storage # thereIsACleanStorage()
Then I should have empty storage # iShouldHaveEmptyStorage()
...
6 scenarios (6 passed)
31 steps (31 passed)
0m0.23s (13.76Mb)
通过注释unset
操作,故意破坏cart:
class Cart extends Component
{
…
public function set($id, $amount)
{
$this->loadItems();
// $this->_items[$id] = $amount;
$this->saveItems();
}
...
}
现在再次运行测试:
Feature: Shopping cart
In order to buy products
As a customer
Feature: Shopping cart
In order to buy products
As a customer
I need to be able to put interesting products into a cart
...
Scenario: Change product count in the cart # features/
cart.feature:31
Given there is a cart with 5 pieces of 7 prod #
thereIsAWhichCostsPs()
When I set 3 pieces for 7 product #
iSetPiecesForProduct()
Then I should have 3 pieces of 7 product #
iShouldHavePiecesOf()
Failed asserting that an array has the subset Array &0 (
7 => 3
).
Scenario: Remove products from the cart # features/
cart.feature:36
Given there is a cart with 5 pieces of 7 prod #
thereIsAWhichCostsPs()
When I add 14 pieces of 7 product #
iAddTheToTheCart()
And I clear cart # iClearCart()
Then I should have empty cart #
iShouldHaveEmptyCart()
--- Failed scenarios:
features/cart.feature:31
6 scenarios (5 passed, 1 failed)
31 steps (30 passed, 1 failed)
0m0.22s (13.85Mb)
在这个例子中,我们看到了一次失败和一次失败报告。
工作原理...
Behat是一个BDD测试框架。它促进writing preceding human-readable testing scenarios to low-level technical implementation。
当我们为每一个特性写场景时,我们可以使用操作的一个集合:
Scenario: Adding products to the cart
Given there is a clean cart
When I add 3 pieces of 5 product
Then I should have 3 pieces of 5 product
And I should have 1 product
And the overall cart amount should be 3
Behat解析我们的句子,并找到相关的实现:
class FeatureContext implements SnippetAcceptingContext
{
/**
* @When I add :pieces of :product
*/
public function iAddTheToTheCart($product, $pieces)
{
$this->cart->add($product, $pieces);
}
}
你可以创建一个单FeatureContext
类(默认),或者为特性集合场景创建指定的上下文的集合。
参考
欲了解更多关于Behat的信息,参考如下URL:
欲了解更多关于其它测试框架的信息,参考本章中的其它小节。