genesis/sql-api-wrapper

此软件包已被废弃,不再维护。作者建议使用 genesis/sql-data-mods 软件包。

SQL/PDO 扩展 - 扩展原始 SQL API 扩展的功能。

3.3.0 2019-01-07 15:12 UTC

README

该想法是为了通过将数据库中的数据操作逻辑与网页界面的交互逻辑分离来简化框架。此扩展提供了一个框架,您可以在其中配置如何与数据库表交互,并提供一个非常容易的上下文类,该类利用此配置来帮助您操作数据。

发布详情

主要版本:通过扩展通道使用初始化器初始化上下文。

次要版本:引入了创建后挂钩。允许为辅助记录提供额外数据。

修补程序:无。

此软件包提供的工具

  • DataModSQLContext - 使用此类提供的步骤定义直接使用您的数据模块。只需在 behat.yml 文件中注册,即可开始。
  • 装饰 API BaseProvider 类 - 用于高级且易于与数据模块集成。
  • DataRetriever 类 - 以稳健的方式检索数据,并快速为您的测试框架打下坚实的基础。

DataModSQLContext

# Insert single entry for a datamod.
Given I have a "User" fixture
# OR with specific data
Given I have a "User" fixture with the following data set:
| name  | Wahab Qureshi              |
| email | its.inevitable@hotmail.com |

# Insert multiple entries for a datamod.
Given I have multiple "User" fixtures with the following data sets:
| name           | email                      |
| Wahab Qureshi  | its.inevitable@hotmail.com |
| Sabhat Qureshi | next-gen-coder@hotmail.com |
| Jawad Qureshi  | to-be-coder@hotmail.com    |

createFixture 调用将尝试在创建另一个之前删除现有记录,因此您始终会得到一个全新的副本。听起来很简单,但外键约束可能不允许这样做。在这些情况下,您可以禁用测试数据库上的外键检查(大多数情况下您不需要这样做)。

安装

composer require --dev genesis/sql-api-wrapper

behat.yml 文件中的示例配置

default:
    suites:
        default:
            contexts:
                - Genesis\SQLExtensionWrapper\DataModSQLContext:
                    debug: false # 1 for all debug, 2 for only SQL queries.
                    userUniqueRef: aq # Optional
    extensions:
        Genesis\SQLExtensionWrapper\Extension:
            connection:
                engine: mysql # mssql, pgsql, sqlite
                host: localhost
                port: 1234
                dbname: mydb
                username: root
                password: root
                schema: myschema
                dbprefix: dev_
            dataModMapping: # Optional
                "*": \QuickPack\DataMod\ # Configure path for all data mods using *.
                "User": \QuickPack\DataMod\User\User # Configure single data mod.

debug - 打开或关闭调试。userUniqueRef: 如果提供的数据是字符串,则将其附加到 fixture 步骤定义中第一个数据列的字符串上。这样,如果多个用户针对单个数据库,则每个用户都有自己的唯一数据。connectionDetails: 您的数据库连接详细信息。dataModMapping: 通过命名空间指向您的数据模块的位置。(可选)

请注意:此扩展期望您将数据模块放在 features/bootstrap/DataMod 文件夹中。如果您有不同的映射,您必须在 composer.json 文件中定义您的自动加载策略,或者手动要求文件。您可以在 php 中设置映射如下

您也可以通过 php 注册上下文文件。

use Behat\Testwork\Hook\Scope\BeforeSuiteScope;
use Genesis\SQLExtensionWrapper\DataModSQLContext;
use Genesis\SQLExtensionWrapper\BaseProvider;

class FeatureContext
{
    /**
     * @BeforeSuite
     */
    public static function loadDataModSQLContext(BeforeSuiteScope $scope)
    {
        BaseProvider::setCredentials([
            'engine' => 'dblib',
            'name' => 'databaseName',
            'schema' => 'dbo',
            'prefix' => 'dev_',
            'host' => 'myhost',
            'port' => '1433',
            'username' => 'myUsername',
            'password' => 'myPassword'
        ]);

        // Default path is \\DataMod\\ which points to features/DataMod/, override this way.
        DataModSQLContext::setDataModMapping([
            '*' => '\\Custom\\DataMod\\'
        ]);

        $scope->getEnvironment()->registerContextClass(
            DataModSQLContext::class,
            ['debug' => false]
        );
    }
}

BaseProvide 类

包装器提供了围绕 behat-sql-extension API 类的强大工具。提供的方法

  • createFixture(array $data = [], string $uniqueColumn = null) // 为新鲜使用重新创建一条记录。可由数据模块覆盖。
  • getSingle(array $where) // 根据映射返回单个记录。
  • getColumn(string $column, array $where) // 从数据库返回单个列值。
  • getValue(string $key) // 根据映射获取键值。
  • truncate() // 截断表。
  • subSelect(string $column, array $where) // 为任何查询提供子查询列的能力。
  • rawSubSelect(string $table, string $column, array $where) // 为任何查询提供子查询列的能力,无需数据修改。
  • saveSession(string $primaryKey) // 保存当前会话以供以后重用。
  • restoreSession() // 恢复由 saveSession 保存的会话。
  • getRequiredData(array $data, string $key, boolean $format) // 扩展:从数组中提取值。
  • getOptionalData(array $data, string $key, mixed $default = null, boolean $format = false) // 扩展:从数组中获取可选值,如果没有提供则提供默认值。
  • getFieldMapping(string $key) // 扩展:获取 getDataMapping 方法中提供的字段映射。
  • getKeyword(string $key) // 获取映射键的键词。

注意:包装器提供的所有方法都是静态的,因为它们具有全局状态 - 我们不需要实例化此包装器。

示例用法

创建一个 DataMod 以在您的上下文文件中使用。这就像从您的 dataMods 中扩展 BaseProvider 类一样简单。

# User.php
<?php

namespace QuickPack\DataMod\User;

use Genesis\SQLExtensionWrapper\BaseProvider;

class User extends BaseProvider
{
    /**
     * Returns the base table to interact with.
     *
     * @return string
     */
    public static function getBaseTable()
    {
        // Ridiculous naming as we find with most databases.
        return 'MySuperApplication.MyUsersNew';
    }

    /**
     * Returns the data mapping for the base table. This is the data that is allowed to be passed in
     * to the data mod. <input> => <mapping>
     *
     * Note any column mapped to '*' is excluded from the queries and only is a part of the data passed around.
     *
     * @return array
     */
    public static function getDataMapping()
    {
        return [
            'id' => 'user_id',
            'name' => 'f_name',
            'email' => 'electronic_address',
            'dateOfBirth' => 'd_o_b',
            'gender' => 'gender',
            'status' => 'real_status',
            'anythingElse' => '*',
            'somethingElse' => '*',
        ];
    }
}

在 PHP 代码中使用 DataMods

您现在可以使用上述 data mods 或直接在步骤定义中使用 PHP 代码。在上下文文件中使用您的 UserDataMod。

# FeatureContext.php
<?php

use Exception;
use QuickPack\DataMod\User\User;

/**
 * Ideally you would want to separate the data step definitions from interactive/assertive step definitions.
 * This is for demonstration only.
 */
class FeatureContext
{
    /**
     * @Given I have a User
     *
     * Use the API to create a fixture user.
     */
    public function createUser()
    {
        // This will create a fixture user.
        // The name will be set to 'Wahab Qureshi'. The rest of the fields if required by the database will be autofilled
        // with fixture data, if they are nullable, null will be stored.
        // If the record exists already, it will be deleted based on the 'name' key provided.
        User::createFixture([
            'name' => 'Wahab Qureshi'
        ], 'name');
    }

    /**
     * @Given I have (number) User(s)
     *
     * Use the API to create random 10 users.
     */
    public function create10Users($count)
    {
        // Save this user's session.
        User::saveSession('id');

        // Create 10 random users.
        for($i = 0; $i < 10; $i++) {
            // Store the ids created for these users maybe?
            $this->userIds[] = User::createFixture();
        }

        // Restore session of the user we created above.
        User::restoreSession();
    }

    /**
     * @Given I should see a User
     *
     * Use the API to retrieve the user created.
     */
    public function assertUserOnPage()
    {
        // Assumptions - we ran the following before running this command:
        // Given I have a User
        // And I have 10 Users

        // Retrieve data created, this will reference the user created by 'Given I have a User' as the session was preserved.
        $id = User::getValue('id');
        $name = User::getValue('name');
        $dateOfBirth = User::getValue('dateOfBirth');
        $gender = User::getValue('gender');

        // Assert that data is on the page.
        $this->assertSession()->assertTextOnPage($id);
        $this->assertSession()->assertTextOnPage($name);
        $this->assertSession()->assertTextOnPage($dateOfBirth);
        $this->assertSession()->assertTextOnPage($gender);
    }

    /**
     * @Given I should see (number) User(s) in the list
     *
     * Consumption of the users created above. For illustration purposes only.
     */
    public function assertUserOnPage($number)
    {
        $usersList = $this->getSession()->getPage()->find('css', '#usersListContainer li');
        $actualCount = count($usersList);

        if ($number !== $actualCount) {
            throw new Exception("Expected to have '$number' users, got '$actualCount'");
        }
    }
}

高级 DataModding

您可以通过以下方式进一步扩展您的 DataMod

<?php

namespace QuickPack\DataMod\User;

use Genesis\SQLExtensionWrapper\BaseProvider;

class User extends BaseProvider
{
    ...

    /**
     * Special Method: Use this method to create auxiliary data off the initial create. This is suitable
     * for creating data where the tables are fragmented.
     *
     * @param int $id The id of the created record.
     * @param array $data The data that was originally passed to create.
     *
     * @return void
     */
    public static function postCreateHook($id, array $data)
    {
        Meta::createFixture(...);
    }

    /**
     * Special Method: This method if implemented is merged with the data provided.
     * Any data provided overwrites the default data.
     * This is a good opportunity to set foreign key values using the subSelect call.
     *
     * @param array $data The data passed in to the data mod.
     *
     * @return array
     */
    public static function getDefaults(array $data)
    {
        return [
            'dateOfBirth' => '1989-05-10',
            'gender' => Gender::subSelect('type', ['id' => 1])
        ];
    }

    /**
     * Method uses subSelect to intelligently select the Id of the status and updates the user record.
     * This is a common case where you want your feature files to be descriptive and won't just pass in id's, use
     * descriptive names instead and infer values in the lower layers.
     *
     * @param string $status The status name (enabled/disabled).
     * @param int $userId The user to update.
     *
     * @return void
     */
    public static function updateStatusById($status, $userId)
    {
        self::update(self::getBaseTable(), [
            'status' => BaseProvider::rawSubSelect('Status', 'id', ['name' => $status])
        ], [
            'id' => $userId
        ])
    }
}

getDefaults() 方法是特殊的,如果存在,它将被自动调用。它允许您为任何列设置默认值。一个例子可能是某种布尔标志,您不想一直定义或可以选择性地覆盖。另一个例子可能是正确设置外键。

构建动态 URL

您可以使用 BaseProvider 类提供的 getKeyword 调用来获取在数据Mod 上定义的键的引用。例如

// We want to create a user and have its id placed in the URL such as '/user/<id>/', so we can visit the page.

// Normally with the above data mod configuration and behat-sql-extension you need to do the following:
$routes = [
    'user' => '/user/{MySuperApplication.MyUsersNew.user_id}/'
];

// Having a data mod gives you a way to abstract any table information 
// by just referencing the data mod itself. The above can be re-written as:
$routes = [
    'user' => '/user/' . User::getKeyword('id') . '/'
];

继续使用标准访问页面步骤定义,使用 genesis/test-routing

    /**
     * @Given I am on the :arg1 page
     * @Given I visit the :arg1 page
     */
    public function iAmOnThePage($arg1)
    {
        $url = Routing::getRoute($arg1, function ($url) {
            return BaseProvider::getApi()->get('keyStore')->parseKeywordsInString($url);
        });
        $this->getMink()->getSession()->visit($url);
    }

高级集成

要使用 Api 的不同版本,您必须充分利用多态性。在您的项目中扩展 BaseProvider 并实现抽象方法 getAPI()。此方法需要返回一个实现 Genesis\SQLExtension\Context\Interfaces\APIInterface 的对象。

# BaseDataMod.php
<?php

use Genesis\SQLExtensionWrapper\BaseProvider;
use Genesis\SQLExtension\Context;

/**
 * Serves as a base class for your own project, makes refactoring easier if you decide to inject your own version of 
 * the API.
 */
abstract class BaseDataMod extends BaseProvider
{
    /**
     * @var array The connection details the API expects.
     */
    public static $connectionDetails;

    /**
     * @var Context\Interfaces\APIInterface
     */
    private static $sqlApi;

    /**
     * @return Context\Interfaces\APIInterface
     */
    public static function getAPI()
    {
        if (! self::$sqlApi) {
            self::$sqlApi = new Context\API(
                new Context\DBManager(Context\DatabaseProviders\Factory(), self::$connectionDetails),
                new Context\SQLBuilder(),
                new Context\LocalKeyStore(),
                new Context\SQLHistory()
            );
        }

        return self::$sqlApi;
    }
}

然后从上述类扩展您的数据模。

数据检索类

数据检索类使得与测试数据集一起工作变得简单,并为传递的参数提供足够的上下文。我们都知道使用数组很痛苦。为了稍微减轻痛苦,我们有以下调用

  • getRequiredData($searchArray, $key) // 隐式数据转换,当没有提供数据时抛出异常。
  • getOptionalData($searchArray, $key, $defaultValue, $format) // 显式数据转换。

为了减轻与 TableNodes 一起工作的痛苦,以下是一些调用

  • loopMultiTable($tableNode, callbackFunction)
  • loopSingleTable($tableNode, callbackFunction)
  • loopPageFieldsTable($tableNode, callbackFunction)
  • transformTableNodeToSingleDataSet($tableNode)
  • transformTableNodeToMultiDataSets($tableNode)

内置大多数常见数据类型的数据转换

  • getFormattedValue($value, $fieldName) // 遵循以下规则
| Fieldname | Conversion                | More info                                                        |
| %Date%    | Format to Y-m-d H:i:s     | This is particularly useful with dynamic dates such as yesterday |
| %Amount%  | To pence, Multiply by 100 | User friendly input such as 100 amount equals 10000 pence        |

使用桥梁

您还可以在框架数据模块和包装器之间设置一个桥梁。您的桥梁必须实现Genesis\SQLExtensionWrapper\BridgeInterface接口才能正常工作。您可以这样注册您的桥梁:

class FeatureContext
{
    public function __construct()
    {
        $bridgeObject = new DoctrineBridge();
        DataModSQLContext::registerBridge($bridgeObject);
    }
}

开发

要开始开发这个项目

部署器 https://github.com/forceedge01/deployer

在项目的根目录下运行

dep use

然后运行

dep project:dev

上述操作将初始化并下载vagrant box作为子模块,启动box,并在其中执行composer install。

运行单元测试

dep project:test

这将运行vagrant box内的单元测试。