PHP 测试套件与伪造测试数据生成指南
1. 编写测试套件
在处理包含大量类和文件的应用程序时,手动运行
phpunit
并指定测试类和 PHP 文件名会变得十分繁琐。不过,
PHPUnit
项目具备使用单一命令运行多个测试的内置功能,这种一组测试的集合被称为测试套件。
1.1 操作步骤
- 步骤 1:将所有测试文件移动到一个文件夹中
mkdir tests
cp *Test.php tests
- 步骤 2:调整包含或引用外部文件的命令,以适应新的文件位置
<?php
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../chap_13_unit_test_simple.php';
class SimpleTest extends TestCase
{
// etc.
-
步骤 3:以目录路径作为参数运行
phpunit,它将自动运行该文件夹中的所有测试
phpunit tests
-
步骤 4:使用
--bootstrap选项指定在运行测试之前执行的文件,常用于启动自动加载
phpunit --boostrap tests_with_autoload/bootstrap.php tests
-
步骤 5:示例
bootstrap.php文件,用于实现自动加载
<?php
require __DIR__ . '/../../Application/Autoload/Loader.php';
Application\Autoload\Loader::init([__DIR__]);
- 步骤 6:使用 XML 配置文件定义一组或多组测试
<phpunit>
<testsuites>
<testsuite name="simple">
<file>SimpleTest.php</file>
<file>SimpleDbTest.php</file>
<file>SimpleClassTest.php</file>
</testsuite>
</testsuites>
</phpunit>
- 步骤 7:另一个示例,基于目录运行测试并指定引导文件
<phpunit bootstrap="bootstrap.php">
<testsuites>
<testsuite name="visitor">
<directory>Simple</directory>
</testsuite>
</testsuites>
</phpunit>
1.2 工作原理
首先确保之前定义的所有测试都已完成。然后创建一个
tests
文件夹,并将所有
*Test.php
文件移动或复制到该文件夹中。接着,按照步骤 2 调整
require_once
语句中的路径。
为了演示
PHPUnit
如何运行文件夹中的所有测试,从包含本章源代码的目录中运行以下命令:
phpunit tests
以下是创建包含自动加载和命名空间的单元测试的详细步骤:
1. 创建一个新的
tests_with_autoload
目录。
2. 在该文件夹中定义一个
bootstrap.php
文件,内容如步骤 5 所示。
3. 在
tests_with_autoload
中创建两个目录:
Demo
和
Simple
。
4. 从本章源代码目录中复制相关文件到
tests_with_autoload/Demo/Demo.php
,并在开头的
<?php
标签后添加
namespace Demo;
。
5. 将
SimpleTest.php
文件复制到
tests_with_autoload/Simple/ClassTest.php
,并修改前几行代码如下:
<?php
namespace Simple;
use Demo\Demo;
use PHPUnit\Framework\TestCase;
class ClassTest extends TestCase
{
protected $demo;
public function setup()
{
$this->demo = new Demo();
}
// etc.
-
创建一个
tests_with_autoload/phpunit.xml文件,内容如下:
<phpunit bootstrap="bootstrap.php">
<testsuites>
<testsuite name="visitor">
<directory>Simple</directory>
</testsuite>
</testsuites>
</phpunit>
- 切换到包含本章代码的目录,运行以下命令:
phpunit -c tests_with_autoload/phpunit.xml
2. 生成伪造测试数据
在测试和调试过程中,融入真实的测试数据是很重要的一部分。特别是在测试数据库访问和生成基准测试时,往往需要大量的测试数据。一种实现方法是从网站抓取数据,然后将这些数据以真实且随机的组合方式插入到数据库中。
2.1 操作步骤
- 步骤 1:确定测试应用程序所需的数据,并考虑网站的受众范围
- 步骤 2:将数据从源转换为可用的数字格式,首选是一系列数据库表,另一个选择是 CSV 文件
- 步骤 3:可以分阶段转换数据,例如从网页上提取国家代码和国家名称列表到文本文件中
- 步骤 4:由于列表较短,可以直接复制粘贴到文本文件中
-
步骤 5:在文本文件中搜索
" "并替换为"\n" -
步骤 6:将文本文件导入电子表格,然后导出为 CSV 文件,再将其导入数据库,例如使用
phpMyAdmin -
步骤 7:创建目标表
prospects的 SQL 语句
CREATE TABLE 'prospects' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'first_name' varchar(128) NOT NULL,
'last_name' varchar(128) NOT NULL,
'address' varchar(256) DEFAULT NULL,
'city' varchar(64) DEFAULT NULL,
'state_province' varchar(32) DEFAULT NULL,
'postal_code' char(16) NOT NULL,
'phone' varchar(16) NOT NULL,
'country' char(2) NOT NULL,
'email' varchar(250) NOT NULL,
'status' char(8) DEFAULT NULL,
'budget' decimal(10,2) DEFAULT NULL,
'last_updated' datetime DEFAULT NULL,
PRIMARY KEY ('id'),
UNIQUE KEY 'UNIQ_35730C06E7927C74' ('email')
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
步骤 8:创建一个能够生成伪造数据的类
FakeData
namespace Application\Test;
use PDO;
use Exception;
use DateTime;
use DateInterval;
use PDOException;
use SplFileObject;
use InvalidArgumentsException;
use Application\Database\Connection;
class FakeData
{
// data generation methods here
}
- 步骤 9:定义常量和属性
const MAX_LOOKUPS = 10;
const SOURCE_FILE = 'file';
const SOURCE_TABLE = 'table';
const SOURCE_METHOD = 'method';
const SOURCE_CALLBACK = 'callback';
const FILE_TYPE_CSV = 'csv';
const FILE_TYPE_TXT = 'txt';
const ERROR_DB = 'ERROR: unable to read source table';
const ERROR_FILE = 'ERROR: file not found';
const ERROR_COUNT = 'ERROR: unable to ascertain count or ID
column missing';
const ERROR_UPLOAD = 'ERROR: unable to upload file';
const ERROR_LOOKUP = 'ERROR: unable to find any IDs in the
source table';
protected $connection;
protected $mapping;
protected $files;
protected $tables;
- 步骤 10:定义用于生成随机字母、街道名称和电子邮件地址的属性
protected $alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected $street1 = ['Amber','Blue','Bright','Broad','Burning',
'Cinder','Clear','Dewy','Dusty','Easy']; // etc.
protected $street2 = ['Anchor','Apple','Autumn','Barn','Beacon',
'Bear','Berry','Blossom','Bluff','Cider','Cloud']; // etc.
protected $street3 = ['Acres','Arbor','Avenue','Bank','Bend',
'Canyon','Circle','Street'];
protected $email1 = ['northern','southern','eastern','western',
'fast','midland','central'];
protected $email2 = ['telecom','telco','net','connect'];
protected $email3 = ['com','net'];
- 步骤 11:在构造函数中接受数据库连接对象和映射数组
public function __construct(Connection $conn, array $mapping)
{
$this->connection = $conn;
$this->mapping = $mapping;
}
- 步骤 12:生成街道名称的方法
public function getAddress($entry)
{
return random_int(1,999)
. ' ' . $this->street1[array_rand($this->street1)]
. ' ' . $this->street2[array_rand($this->street2)]
. ' ' . $this->street3[array_rand($this->street3)];
}
- 步骤 13:生成英国邮政编码的方法
public function getPostalCode($entry, $pattern = 1)
{
return $this->alpha[random_int(0,25)]
. $this->alpha[random_int(0,25)]
. random_int(1, 99)
. ' '
. random_int(1, 9)
. $this->alpha[random_int(0,25)]
. $this->alpha[random_int(0,25)];
}
- 步骤 14:生成伪造电子邮件的方法
public function getEmail($entry, $params = NULL)
{
$first = $entry[$params[0]] ?? $this->alpha[random_int(0,25)];
$last = $entry[$params[1]] ?? $this->alpha[random_int(0,25)];
return $first[0] . '.' . $last
. '@'
. $this->email1[array_rand($this->email1)]
. $this->email2[array_rand($this->email2)]
. '.'
. $this->email3[array_rand($this->email3)];
}
- 步骤 15:生成日期的方法
public function getDate($entry, $params)
{
list($fromDate, $maxDays) = $params;
$date = new DateTime($fromDate);
$date->sub(new DateInterval('P' . random_int(0, $maxDays) . 'D'));
return $date->format('Y-m-d H:i:s');
}
- 步骤 16:从文件中获取数据的方法
public function getEntryFromFile($name, $type)
{
if (empty($this->files[$name])) {
$this->pullFileData($name, $type);
}
return $this->files[$name][
random_int(0, count($this->files[$name]))];
}
- 步骤 17:从文件中提取数据到数组的方法
public function pullFileData($name, $type)
{
if (!file_exists($name)) {
throw new Exception(self::ERROR_FILE);
}
$fileObj = new SplFileObject($name, 'r');
if ($type == self::FILE_TYPE_CSV) {
while ($data = $fileObj->fgetcsv()) {
$this->files[$name][] = trim($data);
}
} else {
while ($data = $fileObj->fgets()) {
$this->files[$name][] = trim($data);
}
}
}
- 步骤 18:从数据库表中获取随机数据的方法
public function getEntryFromTable($tableName, $idColumn, $mapping)
{
$entry = array();
try {
if (empty($this->tables[$tableName])) {
$sql = 'SELECT ' . $idColumn . ' FROM ' . $tableName
. ' ORDER BY ' . $idColumn . ' ASC LIMIT 1';
$stmt = $this->connection->pdo->query($sql);
$this->tables[$tableName]['first'] =
$stmt->fetchColumn();
$sql = 'SELECT ' . $idColumn . ' FROM ' . $tableName
. ' ORDER BY ' . $idColumn . ' DESC LIMIT 1';
$stmt = $this->connection->pdo->query($sql);
$this->tables[$tableName]['last'] =
$stmt->fetchColumn();
}
$result = FALSE;
$count = self::MAX_LOOKUPS;
$sql = 'SELECT * FROM ' . $tableName
. ' WHERE ' . $idColumn . ' = ?';
$stmt = $this->connection->pdo->prepare($sql);
do {
$id = random_int($this->tables[$tableName]['first'],
$this->tables[$tableName]['last']);
$stmt->execute([$id]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
} while ($count-- && !$result);
if (!$result) {
error_log(__METHOD__ . ':' . self::ERROR_LOOKUP);
throw new Exception(self::ERROR_LOOKUP);
}
foreach ($mapping as $key => $value) {
$entry[$value] = $result[$key] ?? NULL;
}
return $entry;
} catch (PDOException $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(self::ERROR_DB);
}
}
- 步骤 19:生成单个伪造数据数组的方法
public function getRandomEntry()
{
$entry = array();
foreach ($this->mapping as $key => $value) {
if (isset($value['source'])) {
switch ($value['source']) {
case self::SOURCE_FILE :
$entry[$key] = $this->getEntryFromFile(
$value['name'], $value['type']);
break;
case self::SOURCE_CALLBACK :
$entry[$key] = $value['name']();
break;
case self::SOURCE_TABLE :
$result = $this->getEntryFromTable(
$value['name'],$value['idCol'],$value['mapping']);
$entry = array_merge($entry, $result);
break;
case self::SOURCE_METHOD :
default :
if (!empty($value['params'])) {
$entry[$key] = $this->{$value['name']}(
$entry, $value['params']);
} else {
$entry[$key] = $this->{$value['name']}($entry);
}
}
}
}
return $entry;
}
- 步骤 20:生成多行伪造数据的方法
public function generateData(
$howMany, $destTableName = NULL, $truncateDestTable = FALSE)
{
try {
if ($destTableName) {
$sql = 'INSERT INTO ' . $destTableName
. ' (' . implode(',', array_keys($this->mapping))
. ') '. ' VALUES ' . ' (:'
. implode(',:', array_keys($this->mapping)) . ')';
$stmt = $this->connection->pdo->prepare($sql);
if ($truncateDestTable) {
$sql = 'DELETE FROM ' . $destTableName;
$this->connection->pdo->query($sql);
}
}
} catch (PDOException $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(self::ERROR_COUNT);
}
for ($x = 0; $x < $howMany; $x++) {
$entry = $this->getRandomEntry();
if ($insert) {
try {
$stmt->execute($entry);
} catch (PDOException $e) {
error_log(__METHOD__ . ':' . $e->getMessage());
throw new Exception(self::ERROR_DB);
}
}
yield $entry;
}
}
2.2 工作原理
首先确保有用于随机数据生成的数据。假设目标表是
prospects
,其 SQL 定义如步骤 7 所示。
作为名称的数据来源,可以创建包含名字和姓氏的文本文件,例如
first_names.txt
和
surnames.txt
。对于城市、州或省、邮政编码和国家,可以从
http://www.geonames.org/
等来源下载数据,并上传到
world_city_data
表中。对于其他字段,如地址、电子邮件、状态等,可以使用
FakeData
类中的方法或定义回调函数。
接下来,定义
Application\Test\FakeData
类,并添加步骤 8 到 29 中讨论的内容。完成后,创建一个名为
chap_13_fake_data.php
的调用程序,设置自动加载并使用相应的类。同时,定义与数据库配置路径和名称文件匹配的常量:
<?php
define('DB_CONFIG_FILE', __DIR__ . '/../config/db.config.php');
define('FIRST_NAME_FILE', __DIR__ . '/../data/files/first_names.txt');
define('LAST_NAME_FILE', __DIR__ . '/../data/files/surnames.txt');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\Test\FakeData;
use Application\Database\Connection;
$mapping = [
'first_name' => ['source' => FakeData::SOURCE_FILE,
'name' => FIRST_NAME_FILE,
'type' => FakeData::FILE_TYPE_TXT],
'last_name' => ['source' => FakeData::SOURCE_FILE,
'name' => LAST_NAME_FILE,
'type' => FakeData::FILE_TYPE_TXT],
'address' => ['source' => FakeData::SOURCE_METHOD,
'name' => 'getAddress'],
'email' => ['source' => FakeData::SOURCE_METHOD,
'name' => 'getEmail',
'params' => ['first_name','last_name']],
'last_updated' => ['source' => FakeData::SOURCE_METHOD,
'name' => 'getDate',
'params' => [date('Y-m-d'), 365*5]],
'phone' => ['source' => FakeData::SOURCE_CALLBACK,
'name' => function () {
return sprintf('%3d-%3d-%4d', random_int(101,999),
random_int(101,999), random_int(0,9999)); }],
'status' => ['source' => FakeData::SOURCE_CALLBACK,
'name' => function () { $status = ['BEG','INT','ADV'];
return $status[rand(0,2)]; }],
'budget' => ['source' => FakeData::SOURCE_CALLBACK,
'name' => function() { return random_int(0, 99999)
+ (random_int(0, 99) * .01); }],
'city' => ['source' => FakeData::SOURCE_TABLE,
'name' => 'world_city_data',
'idCol' => 'id',
'mapping' => [
'city' => 'city',
'state_province' => 'state_province',
'postal_code_prefix' => 'postal_code',
'iso2' => 'country']
],
'state_province'=> [],
'postal_code' => [],
'country' => [],
];
$destTableName = 'prospects';
$conn = new Connection(include DB_CONFIG_FILE);
$fake = new FakeData($conn, $mapping);
foreach ($fake->generateData(10) as $row) {
echo implode(':', $row) . PHP_EOL;
}
3. 测试数据生成的数据源网站
| 数据类型 | URL | 备注 |
|---|---|---|
| 名字 |
http://nameberry.com/
http://www.babynamewizard.com/international-names-lists-popular-names-from-around-the-world | - |
| 原始名字列表 |
http://deron.meranda.us/data/census-dist-female-first.txt
http://deron.meranda.us/data/census-dist-male-first.txt http://www.avss.ucsb.edu/NameFema.HTM http://www.avss.ucsb.edu/namemal.htm | 分别为美国女性和男性名字 |
| 姓氏 |
http://names.mongabay.com/data/1000.html
http://surname.sofeminine.co.uk/w/surnames/most-common-surnames-in-great-britain.html https://gist.github.com/subodhghulaxe/8148971 http://www.dutchgenealogy.nl/tng/surnames-all.php http://www.worldvitalrecords.com/browsesurnames.aspx?l=A | 分别为美国、英国、荷兰等姓氏 |
| 城市 | http://www.travelgis.com/default.asp?framesrc=/cities/ | 世界城市 |
4. 流程图
graph TD;
A[确定测试数据需求] --> B[准备数据来源];
B --> C[创建目标表];
C --> D[定义FakeData类];
D --> E[配置映射数组];
E --> F[生成伪造数据];
通过以上步骤和方法,你可以方便地编写测试套件和生成伪造测试数据,从而更好地进行应用程序的测试和调试。在实际应用中,根据具体需求灵活调整数据来源和生成方法,以满足不同的测试场景。同时,注意代码的优化和错误处理,确保测试过程的稳定性和可靠性。
PHP 测试套件与伪造测试数据生成指南
5. 测试套件与伪造数据生成的应用场景分析
在实际的 PHP 项目开发中,测试套件和伪造测试数据生成有着广泛的应用场景。下面我们通过几个具体的场景来深入了解它们的重要性。
5.1 新功能开发测试
当开发新的功能模块时,为了确保其正确性和稳定性,需要进行全面的测试。通过编写测试套件,可以快速地对新功能进行单元测试。例如,在开发一个用户注册功能时,可以编写一系列的测试用例,包括正常注册、重复注册、注册信息不完整等情况。同时,使用伪造测试数据生成工具,可以为这些测试用例提供各种不同类型的测试数据,如不同格式的电子邮件地址、不同长度的密码等,从而更全面地验证新功能的健壮性。
5.2 数据库迁移测试
在进行数据库迁移时,需要确保数据的完整性和一致性。可以使用测试套件来验证迁移前后的数据是否正确。例如,在将数据从一个旧的数据库表迁移到新的表结构时,可以编写测试用例来检查新表中的数据是否与旧表一致。而伪造测试数据生成则可以帮助模拟大量的真实数据,用于测试数据库迁移的性能和稳定性。
5.3 性能测试
在进行性能测试时,需要模拟大量的用户请求和数据。测试套件可以用于编写性能测试用例,而伪造测试数据生成则可以提供大量的测试数据,以模拟真实的用户场景。例如,在测试一个电商网站的商品搜索功能时,可以使用伪造测试数据生成工具生成大量的商品数据,然后使用测试套件来模拟不同数量的用户同时进行搜索操作,从而评估系统的性能。
6. 常见问题及解决方案
在使用测试套件和伪造测试数据生成的过程中,可能会遇到一些常见的问题。下面我们对这些问题进行分析,并提供相应的解决方案。
6.1 测试套件运行失败
- 问题描述 :在运行测试套件时,可能会出现部分或全部测试用例失败的情况。
-
可能原因
:
- 测试代码存在逻辑错误。
- 被测试的代码发生了变更,导致测试用例不再适用。
- 测试环境与开发环境不一致,例如数据库配置不同。
-
解决方案
:
- 仔细检查测试代码,确保逻辑正确。
- 及时更新测试用例,以适应被测试代码的变更。
- 确保测试环境与开发环境一致,检查数据库配置、依赖库等。
6.2 伪造数据生成异常
- 问题描述 :在生成伪造测试数据时,可能会出现数据格式错误、数据重复等问题。
-
可能原因
:
- 数据生成方法的逻辑存在问题。
- 数据源文件或数据库表的格式不符合要求。
-
解决方案
:
- 检查数据生成方法的代码,确保逻辑正确。
- 验证数据源文件或数据库表的格式,进行必要的调整。
7. 总结与展望
通过本文的介绍,我们了解了如何编写 PHP 测试套件和生成伪造测试数据。测试套件可以帮助我们提高代码的质量和稳定性,而伪造测试数据生成则可以为测试提供丰富的测试数据,使测试更加全面和真实。
在未来的 PHP 开发中,测试和调试将变得越来越重要。随着项目规模的不断增大和复杂度的提高,自动化测试和数据模拟将成为必不可少的工具。同时,我们也可以进一步探索如何优化测试套件和伪造数据生成的性能,提高测试效率。例如,可以使用并行测试技术来加快测试套件的运行速度,使用更智能的算法来生成更真实的伪造数据。
8. 参考流程图
graph LR;
A[新功能开发] --> B[编写测试套件];
B --> C[生成伪造测试数据];
C --> D[执行测试用例];
D --> E{测试结果};
E -- 失败 --> F[检查代码与数据];
F --> B;
E -- 成功 --> G[功能上线];
H[数据库迁移] --> B;
I[性能测试] --> B;
9. 总结表格
| 技术点 | 作用 | 操作步骤 |
|---|---|---|
| 测试套件 | 提高代码质量和稳定性,快速验证功能正确性 |
1. 创建测试文件夹并移动测试文件;2. 调整文件路径;3. 运行
phpunit
命令;4. 使用
--bootstrap
选项;5. 配置 XML 文件
|
| 伪造测试数据生成 | 提供丰富测试数据,模拟真实场景 |
1. 确定数据需求;2. 准备数据来源;3. 创建目标表;4. 定义
FakeData
类;5. 配置映射数组;6. 生成伪造数据
|
通过合理运用测试套件和伪造测试数据生成技术,我们可以更加高效地开发和维护 PHP 项目,确保项目的质量和稳定性。在实际应用中,要根据具体需求灵活调整和优化这些技术,以适应不同的项目场景。
超级会员免费看
80

被折叠的 条评论
为什么被折叠?



