本文目录导读:

我来详细介绍PHP项目的Codeception全栈测试实现方案。
Codeception安装与配置
安装Codeception
# 通过Composer安装 composer require --dev codeception/codeception # 初始化(选择项目类型) vendor/bin/codecept init
项目结构
tests/
├── acceptance/ # 接受测试
├── functional/ # 功能测试
├── unit/ # 单元测试
├── _support/ # 辅助类
│ ├── AcceptanceTester.php
│ ├── FunctionalTester.php
│ └── UnitTester.php
├── _output/ # 测试输出
├── _data/ # 测试数据
└── codeception.yml # 主配置文件
配置文件示例
codeception.yml
namespace: Tests
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
# 数据库配置
params:
- .env
# 模块配置
modules:
config:
Db:
dsn: '%DB_DSN%'
user: '%DB_USER%'
password: '%DB_PASSWORD%'
dump: tests/_data/dump.sql
populate: true
cleanup: true
reconnect: true
测试类型详解
单元测试 (Unit Tests)
<?php
// tests/unit/UserServiceTest.php
class UserServiceTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
protected $tester;
private $userService;
protected function _before()
{
// 模拟Repository
$userRepository = $this->createMock(UserRepositoryInterface::class);
$this->userService = new UserService($userRepository);
}
public function testCreateUser()
{
$userData = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'secure123'
];
$result = $this->userService->create($userData);
$this->assertInstanceOf(User::class, $result);
$this->assertEquals($userData['name'], $result->getName());
}
public function testFindUserById()
{
$userId = 1;
$this->tester->haveRecord('users', [
'id' => $userId,
'name' => 'John Doe',
'email' => 'john@example.com'
]);
$user = $this->userService->findById($userId);
$this->assertNotNull($user);
$this->assertEquals($userId, $user->getId());
}
}
功能测试 (Functional Tests)
<?php
// tests/functional/UserControllerCest.php
class UserControllerCest
{
public function _before(FunctionalTester $I)
{
// 数据库迁移
$I->haveHttpHeader('X-API-Key', 'test-api-key');
// 创建测试用户
$I->haveRecord('users', [
'id' => 1,
'email' => 'test@example.com',
'password' => password_hash('password123', PASSWORD_DEFAULT),
'role' => 'admin'
]);
}
public function testGetUser(FunctionalTester $I)
{
$I->sendGET('/api/users/1');
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContainsJson([
'id' => 1,
'email' => 'test@example.com'
]);
}
public function testCreateUser(FunctionalTester $I)
{
$I->sendPOST('/api/users', [
'name' => 'New User',
'email' => 'newuser@example.com',
'password' => 'secure123'
]);
$I->seeResponseCodeIs(201);
$I->seeResponseIsJson();
// 验证数据库
$I->seeInDatabase('users', [
'email' => 'newuser@example.com'
]);
}
public function testInvalidEmail(FunctionalTester $I)
{
$I->sendPOST('/api/users', [
'email' => 'invalid-email',
'password' => 'secure123'
]);
$I->seeResponseCodeIs(422);
$I->seeResponseContains('The email must be a valid email address.');
}
}
接受测试 (Acceptance Tests)
<?php
// tests/acceptance/LoginCest.php
class LoginCest
{
public function _before(AcceptanceTester $I)
{
// 创建测试数据
$I->haveRecord('users', [
'email' => 'user@example.com',
'password' => password_hash('pass123', PASSWORD_DEFAULT),
'name' => 'Test User'
]);
}
public function testSuccessfulLogin(AcceptanceTester $I)
{
$I->amOnPage('/login');
$I->fillField('email', 'user@example.com');
$I->fillField('password', 'pass123');
$I->click('Login');
$I->seeCurrentUrlEquals('/dashboard');
$I->see('Welcome, Test User');
}
public function testFailedLogin(AcceptanceTester $I)
{
$I->amOnPage('/login');
$I->fillField('email', 'wrong@example.com');
$I->fillField('password', 'wrongpassword');
$I->click('Login');
$I->see('Invalid credentials');
$I->seeCurrentUrlEquals('/login');
}
public function testFormValidation(AcceptanceTester $I)
{
$I->amOnPage('/login');
$I->click('Login'); // 空表单提交
$I->see('The email field is required');
$I->see('The password field is required');
}
}
高级测试模式
数据供给器 (Data Providers)
<?php
class MathTest extends \Codeception\Test\Unit
{
/**
* @dataProvider additionProvider
*/
public function testAddition($a, $b, $expected)
{
$result = $a + $b;
$this->assertEquals($expected, $result);
}
public function additionProvider()
{
return [
'positive numbers' => [1, 2, 3],
'negative numbers' => [-1, -2, -3],
'zero values' => [0, 0, 0],
'mixed values' => [5, -3, 2]
];
}
}
依赖注入测试
<?php
class OrderServiceTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
protected $tester;
public function testOrderCreationWithDependencies()
{
// 模拟依赖
$paymentGateway = $this->createMock(PaymentGatewayInterface::class);
$notificationService = $this->createMock(NotificationServiceInterface::class);
$paymentGateway->method('process')
->willReturn(new PaymentResponse(true, 'txn_123'));
$notificationService->method('send')
->willReturn(true);
$orderService = new OrderService($paymentGateway, $notificationService);
$order = $orderService->create([
'user_id' => 1,
'items' => ['product_1' => 2],
'total' => 100.00
]);
$this->assertInstanceOf(Order::class, $order);
$this->assertEquals('pending', $order->getStatus());
}
}
API测试
<?php
// tests/api/ProductApiCest.php
class ProductApiCest
{
public function testProductCRUD(ApiTester $I)
{
// 认证
$I->haveHttpHeader('Authorization', 'Bearer ' . $this->getToken());
$I->haveHttpHeader('Content-Type', 'application/json');
$I->haveHttpHeader('Accept', 'application/json');
// CREATE
$I->sendPOST('/api/products', [
'name' => 'Test Product',
'price' => 29.99,
'category' => 'electronics'
]);
$I->seeResponseCodeIs(201);
$I->seeResponseIsJson();
// 获取创建的产品ID
$productId = $I->grabDataFromResponseByJsonPath('$.id')[0];
// READ
$I->sendGET('/api/products/' . $productId);
$I->seeResponseCodeIs(200);
$I->seeResponseMatchesJsonType([
'id' => 'integer:>0',
'name' => 'string',
'price' => 'float'
]);
// UPDATE
$I->sendPUT('/api/products/' . $productId, [
'price' => 19.99
]);
$I->seeResponseCodeIs(200);
$I->seeResponseContainsJson(['price' => 19.99]);
// DELETE
$I->sendDELETE('/api/products/' . $productId);
$I->seeResponseCodeIs(204);
}
}
测试套件配置
环境配置文件
# tests/_envs/staging.yml
modules:
config:
Db:
dsn: 'mysql:host=staging-db;dbname=test'
WebDriver:
url: 'https://staging.example.com'
运行特定环境测试
# 运行特定环境 vendor/bin/codecept run --env staging # 运行多个环境 vendor/bin/codecept run --env staging --env production # 并行运行测试 vendor/bin/codecept run --parallel
自定义模块与Helper
创建自定义Helper
<?php
// tests/_support/Helper/CustomHelper.php
namespace Helper;
class CustomHelper extends \Codeception\Module
{
public function loginAsUser($email, $password)
{
$webDriver = $this->getModule('WebDriver');
$webDriver->amOnPage('/login');
$webDriver->fillField('email', $email);
$webDriver->fillField('password', $password);
$webDriver->click('Login');
return $this;
}
public function seeSuccessMessage($message)
{
$webDriver = $this->getModule('WebDriver');
$webDriver->see($message, '.alert-success');
return $this;
}
public function generateTestUser($overrides = [])
{
$defaults = [
'name' => 'Test User ' . uniqid(),
'email' => 'test_' . uniqid() . '@example.com',
'password' => 'password123'
];
return array_merge($defaults, $overrides);
}
}
使用自定义Helper
<?php
class UserTest extends \Codeception\Test\Unit
{
/**
* @var \UnitTester
*/
protected $tester;
public function testUserRegistration()
{
$userData = $this->tester->generateTestUser([
'role' => 'premium'
]);
// 使用自定义断言
$this->tester->assertValidEmail($userData['email']);
$this->tester->assertStrongPassword($userData['password']);
}
}
持续集成配置
GitHub Actions示例
# .github/workflows/test.yml
name: PHP Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test_db
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: mbstring, intl, pdo_mysql
coverage: xdebug
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Setup environment
run: |
cp .env.example .env
php artisan key:generate
- name: Run tests
run: |
vendor/bin/codecept build
vendor/bin/codecept run --coverage --coverage-xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: tests/_output/coverage.xml
最佳实践
测试组织结构
// 使用标签组织测试
/**
* @group api
* @group critical
*/
public function testCriticalApiEndpoint()
{
// 测试代码
}
// 运行特定标签的测试
// vendor/bin/codecept run --group api
// vendor/bin/codecept run --group critical --skip-group slow
测试数据管理
<?php
class TestDataFactory
{
private static $data = [];
public static function createUser($overrides = [])
{
$defaults = [
'name' => 'Test User',
'email' => self::uniqueEmail(),
'password' => 'password123'
];
$userData = array_merge($defaults, $overrides);
self::$data['users'][] = $userData;
return $userData;
}
public static function cleanup()
{
// 清理测试数据
foreach (self::$data as $table => $records) {
// 删除测试数据
}
self::$data = [];
}
private static function uniqueEmail()
{
return 'test_' . uniqid() . '@example.com';
}
}
性能优化技巧
<?php
class PerformanceTest extends \Codeception\Test\Unit
{
// 使用共享fixture
private static $sharedFixtures = [];
public static function setUpBeforeClass()
{
// 只创建一次共享数据
self::$sharedFixtures = createExpensiveFixtures();
}
public function testFeature1()
{
// 使用共享数据
$this->assertNotNull(self::$sharedFixtures['user']);
}
public function testFeature2()
{
// 重用共享数据
$this->assertCount(10, self::$sharedFixtures['items']);
}
}
调试技巧
# 调试模式运行测试 vendor/bin/codecept run --debug # 暂停测试执行 vendor/bin/codecept run --steps # 重建测试套件 vendor/bin/codecept build # 生成测试报告 vendor/bin/codecept run --html --xml
这个全栈测试方案覆盖了Codeception的各种测试类型和最佳实践,能够帮助你在PHP项目中建立完整的测试体系。