View on GitHub

yii2-cookbook-chinese

Yii Application Development Cookbook(Third Edition)中文翻译

使用Behat作单元测试

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

准备

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

如何做…

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

准备扩展结构

  1. 首先,为你的扩展创建一个目录结构:
book
    └── cart
        ├── src
        └── features
  1. 作为一个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"
        }
    }
}
  1. 添加如下内容到book/cart/.gitignore
/vendor
/composer.lock
  1. 安装扩展所有的依赖:
composer install
  1. 现在我们得到如下结构:
book
    └── cart
        ├── src
        ├── features
        ├── .gitignore
        ├── composer.json
        └── vendor

写扩展代码

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

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

book
└── cart
├── src
│   ├── storage
│   │   ├── SessionStorage.php
│   │   └── StorageInterface.php
│   └── Cart.php
├── features
├── .gitignore
├── composer.json
└── vendor

写扩展测试

  1. 添加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';
  1. 创建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
  1. 添加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
  1. 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()]);
    }
}
  1. 此外,在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',
        ]);
    }
}
  1. 添加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;
    }
}
  1. 添加book/cart/behat.yml
default:
    suites:
        default:
            contexts:
                - CartContext
                - StorageContext
  1. 现在我们将得到如下结构:
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:

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