本文目录导读:

我来帮你系统地处理PHP遗留代码的测试补充工作,这是一个循序渐进的过程:
评估现状
首先了解代码库的情况:
// 创建测试清单
$files = [
'legacy_file1.php' => ['complexity' => 'high', 'risk' => 'critical'],
'legacy_file2.php' => ['complexity' => 'medium', 'risk' => 'high'],
// ...
];
优先级排序标准:
- 业务核心功能
- 频繁修改的文件
- 有历史bug的代码
- 与新功能交互的部分
建立测试基础设施
composer.json 配置
{
"require-dev": {
"phpunit/phpunit": "^9.5",
"mockery/mockery": "^1.5",
"phpunit/phpunit-selenium": "*"
},
"autoload": {
"psr-4": {
"App\\": "src/",
"Tests\\": "tests/"
}
}
}
phpunit.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
colors="true"
stopOnFailure="false">
<testsuites>
<testsuite name="Legacy Code Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
测试策略
1 为遗留类创建测试替身
<?php
// tests/Legacy/OrderProcessorTest.php
use PHPUnit\Framework\TestCase;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
class OrderProcessorTest extends TestCase
{
use MockeryPHPUnitIntegration;
public function testProcessOrderWithMockedDatabase()
{
// 创建数据库连接的mock
$dbMock = Mockery::mock('PDO');
$dbMock->shouldReceive('prepare')
->once()
->andReturnSelf();
$stmtMock = Mockery::mock('PDOStatement');
$stmtMock->shouldReceive('execute')->once();
$stmtMock->shouldReceive('fetch')
->once()
->andReturn([
'id' => 1,
'total' => 100,
'status' => 'pending'
]);
$processor = new OrderProcessor($dbMock);
$result = $processor->processOrder(1);
$this->assertEquals('success', $result['status']);
}
}
2 字符集标准化测试
// tests/Legacy/CharacterEncodingTest.php
class CharacterEncodingTest extends TestCase
{
public function testUtf8Conversion()
{
$converter = new LegacyCharacterConverter();
$utf8String = 'Hello 世界';
$isoString = mb_convert_encoding($utf8String, 'ISO-8859-1', 'UTF-8');
$result = $converter->toUtf8($isoString);
$this->assertEquals('Hello 世界', $result);
}
}
3 测试全局状态污染
class GlobalStateTest extends TestCase
{
protected $originalGlobals;
protected function setUp(): void
{
$this->originalGlobals = $_GET;
parent::setUp();
}
protected function tearDown(): void
{
$_GET = $this->originalGlobals;
parent::tearDown();
}
public function testFunctionUsingGlobalGet()
{
$_GET['user_id'] = 42;
$controller = new LegacyController();
$result = $controller->displayUser();
$this->assertEquals(42, $result['id']);
}
}
处理硬编码依赖
重构策略 - 引入接口
// 原始代码
class ReportGenerator {
public function generate() {
$db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$data = $db->query('SELECT * FROM reports');
return $data->fetchAll();
}
}
// 重构后
interface DatabaseInterface {
public function query($sql);
}
class ReportGenerator {
private $database;
public function __construct(DatabaseInterface $database) {
$this->database = $database;
}
public function generate() {
return $this->database->query('SELECT * FROM reports');
}
}
// 测试代码
class ReportGeneratorTest extends TestCase {
public function testGenerate() {
$dbStub = Mockery::mock(DatabaseInterface::class);
$dbStub->shouldReceive('query')
->once()
->with('SELECT * FROM reports')
->andReturn([
['id' => 1, 'name' => 'Report 1']
]);
$generator = new ReportGenerator($dbStub);
$result = $generator->generate();
$this->assertEquals('Report 1', $result[0]['name']);
}
}
渐进式测试方案
阶段1: 基础测试(第一周)
class SmokeTest extends TestCase {
// 确保没有语法错误
public function testAllClassesLoad() {
$classes = ['Order', 'User', 'Product', 'Cart'];
foreach ($classes as $class) {
$this->assertTrue(
class_exists($class) || interface_exists($class),
"Class {$class} should exist"
);
}
}
// 数据库连接测试
public function testDatabaseConnection() {
// 只测试是否能加载数据库配置
$config = include 'config/database.php';
$this->assertArrayHasKey('host', $config);
$this->assertArrayHasKey('dbname', $config);
}
}
阶段2: 持久层测试(第二周)
class LegacyDatabaseTest extends TestCase {
private $db;
protected function setUp(): void {
$this->db = new PDO('sqlite::memory:');
$this->db->exec("CREATE TABLE orders (id INTEGER PRIMARY KEY)");
}
public function testFindOrderById() {
$orderRepository = new LegacyOrderRepository($this->db);
$result = $orderRepository->findById(1);
$this->assertIsArray($result);
}
}
阶段3: 业务逻辑测试(第三周)
class BusinessLogicTest extends TestCase {
public function testPriceCalculation() {
$calculator = new PriceCalculator(
new LegacyTaxCalculator(),
new DiscountService()
);
$result = $calculator->calculate(100, 'standard');
$this->assertEquals(120, $result);
}
}
常用测试技巧
反射测试
class ReflectionTest extends TestCase {
public function testPrivateMethod() {
$obj = new LegacyClass();
$reflection = new ReflectionClass($obj);
$method = $reflection->getMethod('privateMethod');
$method->setAccessible(true);
$result = $method->invoke($obj, 'test');
$this->assertEquals('expected', $result);
}
}
特性测试
trait DatabaseFixtures {
protected function createTestData() {
// 创建测试数据
}
protected function cleanDatabase() {
// 清理数据库
}
}
工具推荐
覆盖率分析
phpunit --coverage-html coverage/
静态分析
composer require --dev phpstan/phpstan vendor/bin/phpstan analyse src --level=max
测试生成器
// 使用PHPUnit's TestCase自动生成 $generator = new \PHPUnit\Util\TestDox\NamePrettifier();
最佳实践清单
- 从小的、低风险的类开始
- 优先测试经常出bug的功能
- 使用依赖注入逐步替换全局依赖
- 创建测试夹具时保留原有的行为
- 不要试图一次性重构后再测试
补测试是渐进的过程,即使是很简单的测试也比没有测试好,每次修改代码时都添加相应的测试,慢慢积累。
需要我详细解释某个特定场景或提供具体的测试方案吗?