本文目录导读:

在ThinkPHP项目中进行单元测试,我推荐使用PHPUnit,以下是完整的编写指南:
环境准备
安装PHPUnit
composer require phpunit/phpunit --dev
创建phpunit.xml配置文件
在项目根目录创建phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Application Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>
创建测试基类
<?php
// tests/TestCase.php
namespace tests;
use think\App;
use think\facade\Db;
use PHPUnit\Framework\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
protected $app;
protected $db;
protected function setUp(): void
{
parent::setUp();
// 初始化应用
$this->app = new App();
$this->app->initialize();
// 使用测试数据库配置
$this->app->config->set([
'type' => 'mysql',
'hostname' => 'localhost',
'database' => 'test_db',
'username' => 'root',
'password' => 'password',
'hostport' => '3306',
], 'database.connections');
// 启动数据库
$this->db = Db::connect('mysql');
}
protected function tearDown(): void
{
// 清理测试数据
$this->db->execute('SET FOREIGN_KEY_CHECKS=0');
foreach ($this->getTables() as $table) {
$this->db->execute("TRUNCATE TABLE `{$table}`");
}
$this->db->execute('SET FOREIGN_KEY_CHECKS=1');
parent::tearDown();
}
protected function getTables()
{
$tables = $this->db->query('SHOW TABLES');
return array_map('reset', $tables);
}
// 辅助方法:创建测试数据
protected function createTestData($model, $data = [])
{
$defaultData = [
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
return $model::create(array_merge($defaultData, $data));
}
}
单元测试示例
模型的单元测试
<?php
// tests/Unit/UserModelTest.php
namespace tests\Unit;
use app\model\User;
use tests\TestCase;
class UserModelTest extends TestCase
{
/**
* 测试创建用户
*/
public function testCreateUser()
{
$user = User::create([
'username' => 'test_user',
'email' => 'test@example.com',
'password' => password_hash('123456', PASSWORD_DEFAULT),
'status' => 1
]);
$this->assertInstanceOf(User::class, $user);
$this->assertEquals('test_user', $user->username);
$this->assertDatabaseHas('users', ['username' => 'test_user']);
}
/**
* 测试查询用户
*/
public function testFindUser()
{
$user = $this->createTestData(User::class, [
'username' => 'find_user',
'email' => 'find@example.com'
]);
$found = User::find($user->id);
$this->assertEquals('find_user', $found->username);
}
/**
* 测试更新用户
*/
public function testUpdateUser()
{
$user = $this->createTestData(User::class, [
'username' => 'update_user'
]);
$user->username = 'updated_user';
$user->save();
$this->assertEquals('updated_user', User::find($user->id)->username);
}
/**
* 测试删除用户
*/
public function testDeleteUser()
{
$user = $this->createTestData(User::class);
$userId = $user->id;
$user->delete();
$this->assertNull(User::find($userId));
}
/**
* 测试数据验证
*/
public function testUserValidation()
{
$this->expectException(\think\exception\ValidateException::class);
$user = new User([
'username' => '', // 空用户名
'email' => 'invalid-email' // 无效邮箱
]);
$user->save();
}
}
服务类的单元测试
<?php
// tests/Unit/UserServiceTest.php
namespace tests\Unit;
use app\service\UserService;
use tests\TestCase;
use app\model\User;
class UserServiceTest extends TestCase
{
protected $userService;
protected function setUp(): void
{
parent::setUp();
$this->userService = new UserService();
}
/**
* @test
* @dataProvider userDataProvider
*/
public function testCreateUser($data, $expected)
{
$result = $this->userService->createUser($data);
$this->assertEquals($expected, $result['success']);
if ($expected) {
$this->assertDatabaseHas('users', ['username' => $data['username']]);
}
}
/**
* 数据提供器
*/
public function userDataProvider()
{
return [
'有效数据' => [
[
'username' => 'valid_user',
'email' => 'valid@example.com',
'password' => 'password123'
],
true
],
'无效数据' => [
[
'username' => '',
'email' => 'invalid',
'password' => 'short'
],
false
]
];
}
/**
* 测试登录逻辑
*/
public function testLogin()
{
// 创建测试用户
$user = User::create([
'username' => 'login_user',
'email' => 'login@example.com',
'password' => password_hash('pass123', PASSWORD_DEFAULT),
'status' => 1
]);
// 正确密码
$result = $this->userService->login('login_user', 'pass123');
$this->assertTrue($result['success']);
// 错误密码
$result = $this->userService->login('login_user', 'wrongpassword');
$this->assertFalse($result['success']);
}
}
控制器的单元测试
<?php
// tests/Feature/UserControllerTest.php
namespace tests\Feature;
use tests\TestCase;
use app\model\User;
class UserControllerTest extends TestCase
{
/**
* 测试用户列表接口
*/
public function testUserList()
{
// 创建测试数据
$this->createTestData(User::class, ['username' => 'user1']);
$this->createTestData(User::class, ['username' => 'user2']);
// 模拟请求
$request = $this->app->request
->withMethod('GET')
->withHeader('X-Requested-With', 'XMLHttpRequest');
$response = $this->app->http->run($request);
$this->assertEquals(200, $response->getCode());
$data = json_decode($response->getContent(), true);
$this->assertCount(2, $data['data']);
}
/**
* 测试用户详情接口
*/
public function testUserDetail()
{
$user = $this->createTestData(User::class, [
'username' => 'detail_user'
]);
$request = $this->app->request
->withMethod('GET')
->withRoute('/api/users/' . $user->id);
$response = $this->app->http->run($request);
$this->assertEquals(200, $response->getCode());
$this->assertStringContainsString('detail_user', $response->getContent());
}
}
数据库测试辅助方法
<?php
// tests/Traits/DatabaseAssertions.php
namespace tests\Traits;
use think\facade\Db;
trait DatabaseAssertions
{
/**
* 断言数据库中存在记录
*/
protected function assertDatabaseHas($table, $conditions)
{
$count = Db::name($table)->where($conditions)->count();
$this->assertGreaterThan(0, $count, sprintf(
'表 %s 中未找到满足条件的数据: %s',
$table,
json_encode($conditions)
));
}
/**
* 断言数据库中不存在记录
*/
protected function assertDatabaseMissing($table, $conditions)
{
$count = Db::name($table)->where($conditions)->count();
$this->assertEquals(0, $count, sprintf(
'表 %s 中找到不期望的数据: %s',
$table,
json_encode($conditions)
));
}
/**
* 断言记录数量
*/
protected function assertDatabaseCount($table, $expectedCount)
{
$actualCount = Db::name($table)->count();
$this->assertEquals($expectedCount, $actualCount, sprintf(
'表 %s 的记录数量不符合预期,期望: %d, 实际: %d',
$table,
$expectedCount,
$actualCount
));
}
}
运行测试
运行所有测试
php vendor/bin/phpunit
运行指定测试文件
php vendor/bin/phpunit tests/Unit/UserModelTest.php
运行指定测试方法
php vendor/bin/phpunit --filter testCreateUser tests/Unit/UserModelTest.php
查看测试覆盖率
php vendor/bin/phpunit --coverage-html coverage
测试配置建议
// config/test.php 测试配置文件
return [
// 测试数据库配置
'database' => [
'type' => 'mysql',
'hostname' => 'localhost',
'database' => 'test_db',
'username' => 'root',
'password' => '',
'hostport' => '3306',
'charset' => 'utf8mb4',
'prefix' => '',
],
// 是否开启调试模式
'debug' => true,
// 测试环境不发送邮件等
'mail' => [
'enabled' => false,
],
];
最佳实践建议
- 数据隔离:每个测试前创建测试数据,测试后清理
- 命名规范:测试方法用
test开头或使用@test注解 - 单一职责:每个测试方法只测试一个功能点
- 使用数据提供器:对于多组测试数据,使用
@dataProvider - 断言明确:使用明确的断言信息,便于调试
- 避免硬编码:尽量减少测试中的硬编码值
- 测试速度:优先使用内存数据库(如SQLite)
这样完整的测试体系能够确保你的ThinkPHP项目核心功能的质量,为后续重构和维护提供安全保障。