genesis/sql-data-mods

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

安装次数: 9,073

依赖关系: 0

建议者: 5

安全性: 0

星标: 2

关注者: 2

分支: 0

开放问题: 15

类型:behat-extension

4.20.0 2020-08-24 18:44 UTC

README

需要固定数据但种子数据造成头痛?即时创建固定种子数据,在场景中保持可见,自动生成数据并处理复杂的数据关系。

在功能文件夹中找到示例用法。

发布详情

主要:允许定义多个数据源。

次要

  • 允许从步骤定义中指定 uniqueColumn,而不是假设第一个列。
  • 通过 dataMod::count() 调用获取计数,现在截断调用是公开的。
  • 将基于域的默认值注入到数据模块中。
  • 如果为套件启用 FailureContext,则失败辅助集成。
  • 数据模块周围使用的字符串值的常量。
  • 简化特性以提供声明的替代语法。

补丁

  • 修复 count 步骤定义以不创建额外记录。
  • 修订了额外的固定步骤定义的英文。

此包提供的工具

  • 定义多个数据源,例如 mssql、mysql 等。
  • DataModSQLContext - 使用此类提供的步骤定义直接使用您的数据模块。只需在 behat.yml 文件中注册即可。
  • 装饰 API BaseProvider 类 - 用于与数据模块的高级和简单集成。
  • DataRetriever 类 - 以稳健的方式检索数据,并快速为您的测试框架奠定坚实基础。
  • CLI 命令以方便调试。
  • 自动生成带或不带数据库支持的数据模块文件。

CLI 命令

仅调试 SQL

  • 要查看由扩展执行的 SQL 语句,请使用 --debug-sql 标志。

调试所有

  • 要查看所有活动以及执行的语句,请使用 --debug-sql-all 标志。

生成数据模块

  • 要生成简单的基数据模块,请使用 --dm-generate <以逗号分隔的表> 标志。
  • 要生成带有数据库支持,请使用 --dm-generate=<以逗号分隔的表> --dm-connection=<连接名称> 标志。
  • 您还可以从帮助菜单中探索 --dm-path--dm-namespace 标志。

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     |
| Another name   | [Users.Email\|Name:Another] |

列表中最后一个值使用外部引用来获取要插入的值。您可以通过阅读 behat-sql-extension 扩展中的“引用外表值”主题来了解更多信息。在这个调用中,我们需要转义管道字符,以免在使用表节点时出现语法错误。

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

安装

composer require --dev genesis/sql-data-mods

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:
            connections:
                mysql:
                    engine: mysql # mssql, pgsql, sqlite...
                    host: '%MYSQL_HOST%' # Resolve environment variable
                    port: 3306
                    dbname: mydb_mysql
                    username: root
                    password: root
                    schema: myschema
                    prefix: dev_
                mssql:
                    engine: mssql # mssql, pgsql, sqlite...
                    host: '%MSSQL_HOST%' # Resolve environment variable
                    port: 1433
                    dbname: mydb_mssql
                    username: root
                    password: root
                    schema: myschema
                    prefix: dev_
            dataModMapping: # Optional
                "*": \QuickPack\DataMod\ # Configure path for all data mods using *.
                "User": \QuickPack\DataMod\User\User # Configure single data mod.
            domainModMapping: # Optional
                "*": \QuickPack\DomainMod\
                "User": \QuickPack\DomainMod\User\User
            FailAid: # If behat-fail-aid is enabled.
                output:
                  enabled: true
                  select: true
                  insert: true
                  update: true
                  delete: true

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

请注意:此扩展程序期望您的dataMods位于features/bootstrap/DataMod文件夹中。如果您有不同的映射方式,您必须在composer.json文件中定义您的autoload策略,或者手动require文件。您可以在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([
            'mssql' => [
                '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\\'
        ]);

        DataModSQLContext::setDomainModMapping([
            '*' => '\\Custom\\DomainMod\\'
        ]);

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

BaseProvide类

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

  • createFixture(array $data = [], string $uniqueColumn = null) // 为新鲜使用重新创建一条记录。可由data mod覆盖。
  • getSingle(array $where) // 返回由映射定义的单条记录。
  • getColumn(string $column, array $where) // 从数据库返回单个列值。
  • getValue(string $key) // 根据映射获取键值。
  • truncate() // 截断表。
  • subSelect(string $column, array $where) // 为任何查询提供子查询列的能力。
  • rawSubSelect(string $table, string $column, array $where) // 为任何查询提供子查询列的能力,无需data mod。
  • 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\DataMods\Traits\SimplifiedDeclarations;
use Genesis\SQLExtensionWrapper\Contract\DataModInterface;
use Genesis\SQLExtensionWrapper\BaseProvider;

class User extends BaseProvider
{
    // Provides an alternate syntax for declarations.
    use SimplifiedDeclarations;

    /**
     * Returns the base table to interact with.
     */
    private static $baseTable = '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
     */
    private static $dataMapping = [
        'id' => 'user_id',
        'name' => 'f_name',
        'email' => 'electronic_address',
        'dateOfBirth' => 'd_o_b',
        'gender' => 'gender',
        'status' => 'real_status',
        'anythingElse' => DataModInterface::NOT_MAPPED,
        'somethingElse' => DataModInterface::NOT_MAPPED,
    ];
}

在PHP代码中使用DataMods

现在您可以使用上面提到的方式或直接在步骤定义中使用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
{
    ...

    /**
     * If you've defined multiple connections, you can specify which connection to use for each of your
     * data mods.
     *
     * @return string
     */
    public static function getConnectionName()
    {
        return 'mssql';
    }

    /**
     * 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.
     *
     * Similar methods are available:
     * - getSelectDefaults()
     * - getUpdateDefaults()
     * - getDeleteDefaults()
     *
     * @param array $data The data passed in to the data mod.
     *
     * @return array
     */
    public static function getInsertDefaults(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
        ])
    }
}

getInsertDefaults()方法特别,如果存在,将自动调用。它允许您为任何列设置默认值。一个例子可能是一个布尔标志,您不希望反复定义或可选项地覆盖。另一个例子可能是设置外键或对某些操作提出要求。您为每个操作类型(即选择、更新、插入和删除)都有此方法。

组合Data mods(领域Mod)

有时我们的数据分散在几个表中,但我们不希望这种分散渗透到我们的测试文件中。为了方便这种情况,我们有领域mods。

<?php

namespace App\Tests\Behaviour\DomainMod;

use App\Tests\Behaviour\DataMod;
use Genesis\SQLExtensionWrapper\Contract\DomainModInterface;

class User implements DomainModInterface
{
    public static function getDataMods()
    {
        return [
            DataMod\User::class,
            DataMod\UserExt::class,
        ];
    }
}

支持此功能的步骤定义如下

Scenario: ...
    Given I have a "Ship" domain fixture
    Given I have an additional "Ship" domain fixture
    Given I have a "Ship" domain fixture with the following data set:
    | name | ABC |

您可以通过领域mods注入data mod默认值,这些默认值优先于data mod默认值,但低于步骤定义提供的值。

<?php

namespace App\Tests\Behaviour\DomainMod;

use App\Tests\Behaviour\DataMod;
use Genesis\SQLExtensionWrapper\Contract\DomainModInterface;

class User implements DomainModInterface
{
    public static function getInsertDefaults()
    {
        return [
            DataMod\User::class => [
                'age' => 31
            ],
            DataMod\UserExt::class => [
                'name' => 'Jack'
            ],
        ];
    }

    public static function getDataMods()
    {
        return [
            DataMod\User::class,
            DataMod\UserExt::class,
        ];
    }
}

构建动态URL

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

// 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;
    }
}

然后从上面的类扩展您的data mods。

数据检索类

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

  • 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\Contract\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内的单元测试。