圣泉山人 后端开发工程师

PHP单元测试(二) 数据库测试

2018-07-02

数据库测试

数据库测试的四个阶段:

  • 建立基境
  • 执行被测系统
  • 验证结果
  • 拆除基境

PHPUnit 支持的数据库:

  • MySQL
  • PostgreSQL
  • Oracle
  • SQLite

可以集成: Zend FrameworkDoctrine 2 来支持其他数据库,比如:Microsoft SQL Server

PHPUnit 操作步骤:

  • 连接数据库
  • ==对所有指定表执行 TRUNCATE 来清空数据表==
  • 迭代所有指定的基境数据行并将其插入到对应的表里
  • 所有数据库都完成重置并加载好初始状态后,开始执行实际的测试

PHPUnit 数据库连接

由于会用到数据库扩展模块,因此需要安装 DbUnit 组件包:

composer require --dev phpunit/dbunit

==PHPUnit 要求在测试套件开始时所有数据库对象必须全部可用。数据库、表、序列、触发器还有视图,必须全部在运行测试套件之前创建好==

数据库扩展模块的测试类例:

use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
class TestDemo extends TestCase{
    use TestCaseTrait;

    /**
     * 创建一个数据库连接,createDefaultDBConnection 需要 PDO实例 和 第二个可选参数:数据库名
     * @return \PHPUnit\DbUnit\Database\DefaultConnection
     */
    protected function getConnection()
    {
        $dbms='mysql';          //数据库类型
        $host='192.168.33.12';  //数据库主机名
        $dbName='test';         //使用的数据库
        $user='root';           //数据库连接用户名
        $pass='123456';         //对应的密码
        $dsn="$dbms:host=$host;dbname=$dbName";
        $pdo = new PDO($dsn, $user, $pass);
        return $this->createDefaultDBConnection($pdo, $dbName);
    }

    /**
     * 实现数据集:多种方式
     */
    protected function getDataSet() {}
}

进阶版1:由于数据库连接比较稳定,可以做一个c抽象类来重用:

use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

abstract class MyAppDBTestCase extends TestCase {
    use TestCaseTrait;

    static private $pdo = null;
    private $conn = null;

    protected function getConnection()
    {
        if ($this->conn !== null) {
            return $this->conn;
        }

        if (self::$pdo == null) {
            $dbms='mysql';          //数据库类型
            $host='192.168.33.12';  //数据库主机名
            $dbName='test';         //使用的数据库
            $user='root';           //数据库连接用户名
            $pass='123456';         //对应的密码
            $dsn="$dbms:host=$host;dbname=$dbName";
            self::$pdo = new PDO($dsn, $user, $pass);
        }

        $this->conn = $this->createDefaultDBConnection(self::$pdo);
        return $this->conn;
    }
}

进阶版2:将数据库连接信息放入配置文件 比如:phpunit.xml

<?xml version="1.0" encoding="UTF-8" ?>
<phpunit 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
>
    <php>
        <var name="DB_DSN" value="mysql:dbname=test;host=192.168.33.12" />
        <var name="DB_USER" value="root" />
        <var name="DB_PASSWD" value="123456" />
        <var name="DB_DBNAME" value="test" />
    </php>
</phpunit>

使用配置:

protected function getConnection()
{
    if ($this->conn !== null) {
        return $this->conn;
    }

    if (self::$pdo == null) {
        self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
    }

    $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
    return $this->conn;
}

如此配置后,还可以通过在命令行指定不同的配置来运行测试套件:

phpunit --configuration developer-a.xml MyTests/
phpunit --configuration developer-b.xml MyTests/

DataSet 和 DataTable

DataSet(数据集)和 DataTable(数据表)是围绕着数据库表、行、列的抽象层

在测试中,数据库断言的工作流由以下三个简单的步骤组成:

  • 用表名称来指定数据库中的一个或多个表(实际上是指定了一个数据集)
  • 用你喜欢的格式(YAML、XML等等)来指定预期数据集
  • 断言这两个数据集陈述是彼此相等的。

有三种不同类型的 DataSet/DataTable

  • 基于文件的 DataSetDataTable
    • Flat XML DataSet (平直 XML 数据集) ==存在:NULL 问题==
    • XML DataSet (XML 数据集)
    • MySQL XML DataSet (MySQL XML 数据集)
    • YAML DataSet (YAML 数据集)
    • CSV DataSet (CSV 数据集) ==存在:NULL 问题==
    • Array DataSe (数组数据集)
  • 基于查询的 DataSetDataTable
    • Query (SQL) DataSet (查询(SQL)数据集)
    • Database (DB) Dataset (数据库数据集)
  • 筛选与组合 DataSetDataTable
    • DataSet Filter (数据集筛选器)
    • Composite DataSet (组合数据集)

Flat XML DataSetCSV DataSet 的 NULL 问题可以通过 :Replacement DataSet (替换数据集) 解决

Flat XML DataSet

存在 NULL 问题,建议只在不需要使用 NULL 值时,才使用。

datasets/myFlatXml.xml

<?xml version="1.0" ?>
<dataset>
    <!-- 根节点下,每个标签就代表一行数据,标签名=表名,每个属性=一个列 -->
    <user_info id="1" user="Jack" age="23" sex="male" />
    <user_info id="2" user="Rose" age="19" sex="female" />
    <user_info id="3" user="Lucy" age="20" sex="female" />
    <user_info id="4" user="Lilei" age="25" sex="male" />
</dataset>

使用方式:

protected function getDataSet()
{
    return $this->createFlatXMLDataSet("datasets/myFlatXml.xml");
}
XML DataSet
<?xml version="1.0" encoding="utf-8" ?>
<dataset>
    <table name="user_info">
        <column>id</column>
        <column>user</column>
        <column>age</column>
        <column>sex</column>
        <row>
            <value>1</value>
            <value>Lucy</value>
            <value>18</value>
            <!-- 通过 null标签指定null值 -->
            <null />
        </row>
        <row>
            <value>2</value>
            <value>Jack</value>
            <value>18</value>
            <value>male</value>
        </row>
    </table>
</dataset>

使用方式:

protected function getDataSet()
{
    return $this->createXMLDataSet("datasets/myXml.xml");
}

public function testData()
{
    $this->assertTableRowCount('user_info', 3, 'table row count not 4');
}
MySQL XML DataSet

MySQL 数据库服务器专用。从 PHPUnit 3.5 开始支持

可以用 mysqldump 工具来生成这种格式的文件。与 mysqldump 支持的 CSV 数据集不同,这种 XML 格式可以在单个文件中包含多个表的数据。要生成这种格式的文件,可以这样调用 mysqldump

mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml

示例:

> mysqldump --xml -t -uroot -p test > /vagrant/mysqlXml.xml
> Enter password:

导出的 mysqlXml.xml 内容:

<?xml version="1.0"?>
<mysqldump xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<database name="test">
	<table_data name="user_info">
	<row>
		<field name="id">1</field>
		<field name="user">Lucy</field>
		<field name="age">18</field>
		<field name="sex" xsi:nil="true" />
	</row>
	<row>
		<field name="id">2</field>
		<field name="user">Jack</field>
		<field name="age">18</field>
		<field name="sex">male</field>
	</row>
	</table_data>
</database>
</mysqldump>

使用方式:

protected function getDataSet() {
    return $this->createMySQLXMLDataSet("datasets/mysqlXml.xml");
}
YAML DataSet
user_info:
  -
    id: 1
    user: "李白"
    age: 30
    sex: "male"
  -
    id: 2
    user: "貂蝉"
    age: 19
    sex:

==注意:每个字段后跟的 : 后面必须跟一个空格,在 PHPStorm 中能看到变色即正常,否则可能无法解析==

使用方式:

protected function getDataSet() {
    //没有提供 create 方法,需要手动 new
    return new YamlDataSet("datasets/myYaml.yml");
}
CSV DataSet
id,user,age,sex
1,"貂蝉",23,"female"
2,"吕布",25,"male"

使用方式:

protected function getDataSet() {
    $dataSet = new CsvDataSet();
    $dataSet->addTable('user_info', "datasets/myCsv.csv");
    return $dataSet;
}

==CSV 方式也存在 NULL 问题:无法直接给定字段 NULL 值==

Array DataSe

在 PHPUnit 的数据库扩展中,还没有基于数组的 DataSet,不过很容易自行实现:

namespace Tests;
use PHPUnit\DbUnit\DataSet\AbstractDataSet;
use PHPUnit\DbUnit\DataSet\DefaultTableIterator;
use PHPUnit\DbUnit\InvalidArgumentException;
use PHPUnit\DbUnit\DataSet\DefaultTableMetadata;
use PHPUnit\DbUnit\DataSet\DefaultTable;

class MyAppDbUnitArrayDataSet extends AbstractDataSet
{
    protected $tables = [];

    public function __construct(array $data)
    {
        foreach ($data as $tableName => $rows) {
            $columns = [];
            if (isset($rows[0])) {
                $columns = array_keys($rows[0]);
            }

            $metaData = new DefaultTableMetadata($tableName, $columns);
            $table = new DefaultTable($metaData);

            foreach ($rows as $row) {
                $table->addRow($row);
            }
            $this->tables[$tableName] = $table;
        }
    }

    protected function createIterator($reverse = false)
    {
        return new DefaultTableIterator($this->tables, $reverse);
    }

    public function getTable($tableName)
    {
        if (!isset($this->tables[$tableName])) {
            throw new InvalidArgumentException("$tableName is not a table in the current databases");
        }
        return $this->tables[$tableName];
    }
}

使用:

protected function getDataSet() {
   //数组数据集
    return new MyAppDbUnitArrayDataSet([
        'user_info' => [
            [
                'id' => 1,
                'user' => "李元芳",
                'age' => 16,
                'sex' => "male",
            ],
            [
                'id' => 2,
                'user' => "狄仁杰",
                'age' => 20,
                'sex' => null,//很容易处理 NULL 值
            ],
        ],
    ]);
}
Query (SQL) DataSet

通过查询的方式获取数据集

protected function getDataSet() {
    $dataset = new QueryDataSet($this->getConnection());
    $dataset->addTable('user_info', "SELECT * FROM user_info_copy ORDER BY ID DESC");
    return $dataset;
}
Database (DB) Dataset

从其他库中获取的全部表或者指定表数据作为数据集

protected function getDataSet() {
    //可以指定表或者不指定
    $tableNames = ['user_info'];
    return $this->getConnectionDemo()->createDataSet($tableNames);
}

public function getConnectionDemo()
{
    $pdo = new \PDO("mysql:dbname=demo;host=192.168.33.12", "root", "123456");
    return $this->createDefaultDBConnection($pdo, 'demo');
}
通过 Replacement DataSet 处理 NULL 问题

Flat XMLCSV DataSet 所存在的 NULL 问题,可以通过:Replacement DataSet来解决,它是一个数据集的修饰器,可以将数据集中任意列的值替换为其他替代之。

示例,有如下 Csv 的数据集内容:

id,user,age,sex
1,"貂蝉",23,"female"
2,"吕布",25,"##NULL##"
protected function getDataSet() {
    $dataset = new CsvDataSet();
    $dataset->addTable('user_info', "datasets/myCsv.csv");
    //通过修饰器替换指定字符串
    $replaceDataSet = new ReplacementDataSet($dataset);
    $replaceDataSet->addFullReplacement("##NULL##", null);
    //返回修饰后的数据集
    return $replaceDataSet;
}
DataSet Filter

数据集筛选器作用:为需要包含在子数据集中的表和列指定白/黑名单。与 DB DataSet 联用来对数据集中的列进行筛选尤其方便。

白名单设置:

protected function getDataSet() {
    $tableNames  = ['user_info'];
    $dataSet = $this->getConnectionDemo()->createDataSet();
    $filterDataSet = new Filter($dataSet);
    //添加表的白名单过滤规则
    $filterDataSet->addIncludeTables($tableNames);
    //添加指定表的字段过滤规则
    $filterDataSet->setIncludeColumnsForTable('user_info', ['id', 'user']);
    return $filterDataSet;
}

public function getConnectionDemo() {
    $pdo = new \PDO("mysql:dbname=demo;host=192.168.33.12", "root", "123456");
    return $this->createDefaultDBConnection($pdo, 'demo');
}

黑名单设置:

protected function getDataSet() {
    $tableNames  = ['other'];
    $dataSet = $this->getConnectionDemo()->createDataSet();
    $filterDataSet = new Filter($dataSet);
    //添加表的黑名单过滤规则:不处理 other 表
    $filterDataSet->addExcludeTables($tableNames);
    //添加指定表的字段的黑名单过滤规则:不处理:'age','sex','user'这3个字段
    $filterDataSet->setExcludeColumnsForTable('user_info', ['age','sex','user']);
    return $filterDataSet;
}

注意:

  • 不能对同一个表同时应用排除与包含两种列筛选器,只能分别应用于不同的表
  • 表的白名单和黑名单也只能选择其一,不能二者同时使用
Composite DataSet

组合数据集: 能将多个已存在的数据集聚合成单个数据集。

如果多个数据集中存在同样的表,其中的数据行将按照指定的顺序进行追加。如果存在主键/唯一键 冲突的话,会报错,无法被处理。

示例,有如下两个数据子集:

myYaml.yml:

user_info:
  -
    id: 1
    user: "刘备"
    age: 32
    sex: "male"

myYaml.yml2:

user_info:
  -
    id: 2
    user: "关羽"
    age: 32
    sex: "male"

合并方式:

protected function getDataSet() {
    $dataSet1 = new YamlDataSet("datasets/myYaml.yml");
    $dataSet2 = new YamlDataSet("datasets/myYaml2.yml");

    //将两个数据集合并在一起
    $compositeDataSet = new CompositeDataSet();
    $compositeDataSet->addDataSet($dataSet1);
    $compositeDataSet->addDataSet($dataSet2);

    return $compositeDataSet;
}

外键问题

在建立基境的过程中, PHPUnit 的数据库扩展模块按照基境中所指定的顺序将数据行插入到数据库内。假如数据库中使用了外键,这就意味着必须指定好表的顺序,以避免外键约束失败。

相关API

数据库连接 API:

  • createDataSet
  • createQueryTable 通过获得的 QueryTable 对象进行 ==表断言== ,以此来核实具体的字段等是否正确
  • getRowCount

断言相关API:

  • assertTablesEqual 断言两个 QueryTable 对象是否一样,即表数据是否一样
    • 准备一个结果数据集文件,比如:expectedRes.yml
        user_info:
          -
            id: 1
            user: "李白1"
            age: 30
            sex: "male"
          -
            id: 2
            user: "貂蝉"
            age: 19
            sex:
      
    • 比对查询出来的表对象(代表表中实际的数据) 和 期望的结果数据(expectedRes.yml)中的数据
        public function testData()
        {
            $queryTable = $this->getConnection()->createQueryTable('user_info', "SELECT * FROM user_info WHERE id = 1");
          
            $expectedTable = (new YamlDataSet("datasets/expectedRes.yml"))->getTable('user_info');
          
            $this->assertTablesEqual($expectedTable, $queryTable);
        }
      
    • 结果不一致的示例:

      image

  • assertDataSetsEqual 通过断言数据集,可达到验证多个表数据的效果
      public function testCreateDataSetAssertion()
      {
          $dataSet = $this->getConnection()->createDataSet(['guestbook']);
          $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml');
          $this->assertDataSetsEqual($expectedDataSet, $dataSet);
      }
    

Similar Posts

Comments