PHP项目接口自动化测试如何搭建

wen PHP项目 26

本文目录导读:

PHP项目接口自动化测试如何搭建

  1. 整体架构设计
  2. 基础环境搭建
  3. 核心代码实现
  4. 运行测试
  5. 最佳实践建议

我来详细说明PHP项目接口自动化测试的搭建方案。

整体架构设计

PHP项目接口自动化测试框架
├── 测试框架:PHPUnit + GuzzleHttp
├── 报告生成:Allure/PHPUnit HTML报告
├── CI集成:Jenkins/GitLab CI
└── 数据管理:JSON/YAML/数据库

基础环境搭建

安装依赖

composer require --dev phpunit/phpunit
composer require guzzlehttp/guzzle
composer require --dev phpunit/phpunit-selenium
composer require --dev allure-framework/allure-phpunit

目录结构

tests/
├── Api/
│   ├── Base/
│   │   └── ApiTestCase.php
│   ├── User/
│   │   ├── UserLoginTest.php
│   │   └── UserRegisterTest.php
│   ├── Order/
│   │   └── OrderCreateTest.php
│   └── ...
├── Data/
│   ├── user_data.json
│   └── api_config.php
├── Helper/
│   ├── ApiHelper.php
│   └── AssertHelper.php
├── Report/
└── phpunit.xml

核心代码实现

基础测试类

<?php
// tests/Api/Base/ApiTestCase.php
namespace Tests\Api\Base;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use PHPUnit\Framework\TestCase;
class ApiTestCase extends TestCase
{
    protected $httpClient;
    protected $baseUrl;
    protected $headers = [];
    protected $token;
    protected function setUp(): void
    {
        parent::setUp();
        // 初始化HTTP客户端
        $this->baseUrl = getenv('API_BASE_URL') ?: 'http://localhost:8000/api';
        $this->httpClient = new Client([
            'base_uri' => $this->baseUrl,
            'timeout' => 30,
            'http_errors' => false,
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ]
        ]);
    }
    // 发送GET请求
    protected function get($uri, $params = [], $headers = [])
    {
        try {
            $response = $this->httpClient->get($uri, [
                'query' => $params,
                'headers' => array_merge($this->headers, $headers)
            ]);
            return $this->handleResponse($response);
        } catch (RequestException $e) {
            return $this->handleException($e);
        }
    }
    // 发送POST请求
    protected function post($uri, $data = [], $headers = [])
    {
        try {
            $response = $this->httpClient->post($uri, [
                'json' => $data,
                'headers' => array_merge($this->headers, $headers)
            ]);
            return $this->handleResponse($response);
        } catch (RequestException $e) {
            return $this->handleException($e);
        }
    }
    // 发送PUT请求
    protected function put($uri, $data = [], $headers = [])
    {
        try {
            $response = $this->httpClient->put($uri, [
                'json' => $data,
                'headers' => array_merge($this->headers, $headers)
            ]);
            return $this->handleResponse($response);
        } catch (RequestException $e) {
            return $this->handleException($e);
        }
    }
    // 发送DELETE请求
    protected function delete($uri, $headers = [])
    {
        try {
            $response = $this->httpClient->delete($uri, [
                'headers' => array_merge($this->headers, $headers)
            ]);
            return $this->handleResponse($response);
        } catch (RequestException $e) {
            return $this->handleException($e);
        }
    }
    // 处理响应
    protected function handleResponse($response)
    {
        $body = $response->getBody()->getContents();
        $statusCode = $response->getStatusCode();
        return [
            'status' => $statusCode,
            'body' => json_decode($body, true),
            'headers' => $response->getHeaders()
        ];
    }
    // 处理异常
    protected function handleException($e)
    {
        return [
            'status' => 500,
            'body' => [
                'error' => 'Request failed',
                'message' => $e->getMessage()
            ],
            'headers' => []
        ];
    }
    // 设置认证Token
    protected function setToken($token)
    {
        $this->token = $token;
        $this->headers['Authorization'] = 'Bearer ' . $token;
    }
    // 清理认证
    protected function clearAuth()
    {
        unset($this->headers['Authorization']);
    }
}

API辅助类

<?php
// tests/Helper/ApiHelper.php
namespace Tests\Helper;
class ApiHelper
{
    // 生成随机数据
    public static function generateRandomEmail()
    {
        return 'test_' . uniqid() . '@example.com';
    }
    public static function generateRandomPhone()
    {
        $prefix = ['13', '15', '17', '18', '19'];
        return $prefix[array_rand($prefix)] . str_pad(rand(0, 999999999), 9, '0', STR_PAD_LEFT);
    }
    // 从JSON文件加载测试数据
    public static function loadTestData($filename, $key = null)
    {
        $path = __DIR__ . '/../Data/' . $filename . '.json';
        if (!file_exists($path)) {
            throw new \Exception("Test data file not found: {$path}");
        }
        $data = json_decode(file_get_contents($path), true);
        if ($key) {
            return $data[$key] ?? null;
        }
        return $data;
    }
    // 格式化响应结果
    public static function formatResponse($response)
    {
        return "Status: {$response['status']}\n" .
               "Body: " . json_encode($response['body'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    }
}

断言辅助类

<?php
// tests/Helper/AssertHelper.php
namespace Tests\Helper;
use PHPUnit\Framework\Assert;
class AssertHelper
{
    // 验证成功响应
    public static function assertSuccess($response, $code = 200)
    {
        Assert::assertEquals($code, $response['status'], '状态码不符');
        Assert::assertTrue(
            isset($response['body']['success']) || $response['status'] < 400,
            '响应标记为非成功'
        );
    }
    // 验证错误响应
    public static function assertError($response, $code = 400)
    {
        Assert::assertTrue(
            in_array($response['status'], [$code, 401, 403, 404, 422, 500]),
            '非预期错误码'
        );
    }
    // 验证JSON结构
    public static function assertJsonStructure($response, array $structure)
    {
        $body = $response['body'];
        foreach ($structure as $key => $type) {
            Assert::assertArrayHasKey($key, $body, "缺少键: {$key}");
            if (is_string($type)) {
                switch ($type) {
                    case 'string':
                        Assert::assertIsString($body[$key], "{$key} 应为字符串");
                        break;
                    case 'integer':
                        Assert::assertIsInt($body[$key], "{$key} 应为整数");
                        break;
                    case 'array':
                        Assert::assertIsArray($body[$key], "{$key} 应为数组");
                        break;
                    case 'boolean':
                        Assert::assertIsBool($body[$key], "{$key} 应为布尔值");
                        break;
                }
            } elseif (is_array($type)) {
                // 递归验证嵌套结构
                self::assertJsonStructure(
                    ['body' => $body[$key]],
                    $type
                );
            }
        }
    }
    // 验证分页响应
    public static function assertPagination($response)
    {
        $body = $response['body'];
        Assert::assertArrayHasKey('data', $body, '缺少data字段');
        Assert::assertArrayHasKey('meta', $body, '缺少meta字段');
        Assert::assertIsArray($body['data'], 'data应为数组');
        Assert::assertIsArray($body['meta'], 'meta应为数组');
        $meta = $body['meta'];
        Assert::assertArrayHasKey('current_page', $meta);
        Assert::assertArrayHasKey('last_page', $meta);
        Assert::assertArrayHasKey('per_page', $meta);
        Assert::assertArrayHasKey('total', $meta);
    }
}

测试用例示例

<?php
// tests/Api/User/UserLoginTest.php
namespace Tests\Api\User;
use Tests\Api\Base\ApiTestCase;
use Tests\Helper\ApiHelper;
use Tests\Helper\AssertHelper;
class UserLoginTest extends ApiTestCase
{
    /**
     * @test
     * @group user
     * @group login
     */
    public function testSuccessfulLogin()
    {
        // 准备测试数据
        $data = [
            'email' => 'test@example.com',
            'password' => 'password123'
        ];
        // 发送请求
        $response = $this->post('/user/login', $data);
        // 验证响应
        AssertHelper::assertSuccess($response);
        $body = $response['body'];
        // 验证返回结构
        AssertHelper::assertJsonStructure($response, [
            'success' => 'boolean',
            'message' => 'string',
            'data' => [
                'token' => 'string',
                'user' => [
                    'id' => 'integer',
                    'name' => 'string',
                    'email' => 'string'
                ]
            ]
        ]);
        // 保存Token供后续测试使用
        $this->setToken($body['data']['token']);
        echo ApiHelper::formatResponse($response);
    }
    /**
     * @test
     * @group user
     * @group login
     * @dataProvider invalidLoginDataProvider
     */
    public function testLoginWithInvalidData($data, $expectedStatus, $expectedError)
    {
        $response = $this->post('/user/login', $data);
        AssertHelper::assertError($response, $expectedStatus);
        $body = $response['body'];
        // 验证错误信息
        $this->assertArrayHasKey('message', $body);
        $this->assertStringContainsString($expectedError, $body['message']);
    }
    /**
     * 无效登录数据提供器
     */
    public function invalidLoginDataProvider()
    {
        return [
            'missing_email' => [
                ['password' => 'password123'],
                422,
                '邮箱不能为空'
            ],
            'missing_password' => [
                ['email' => 'test@example.com'],
                422,
                '密码不能为空'
            ],
            'wrong_password' => [
                ['email' => 'test@example.com', 'password' => 'wrong'],
                401,
                '密码错误'
            ],
            'invalid_format' => [
                ['email' => 'not-email', 'password' => '123'],
                422,
                '邮箱格式不正确'
            ]
        ];
    }
    /**
     * @test
     * @group user
     * @group login
     */
    public function testLoginWithTooManyAttempts()
    {
        // 模拟多次登录失败
        for ($i = 0; $i < 5; $i++) {
            $response = $this->post('/user/login', [
                'email' => 'test@example.com',
                'password' => 'wrong_password_' . $i
            ]);
        }
        // 第6次应该被限制
        $response = $this->post('/user/login', [
            'email' => 'test@example.com',
            'password' => 'password123'
        ]);
        AssertHelper::assertError($response, 429);
        $this->assertStringContainsString('尝试次数过多', $response['body']['message']);
    }
}

数据库测试数据管理

<?php
// tests/Helper/TestDataManager.php
namespace Tests\Helper;
use PDO;
class TestDataManager
{
    private $pdo;
    private static $instance = null;
    private function __construct()
    {
        // 使用测试数据库配置
        $this->pdo = new PDO(
            sprintf(
                'mysql:host=%s;dbname=%s_test;charset=utf8mb4',
                getenv('DB_HOST') ?: 'localhost',
                getenv('DB_NAME') ?: 'api_test'
            ),
            getenv('DB_USER') ?: 'root',
            getenv('DB_PASS') ?: '',
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ]
        );
    }
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    // 创建测试用户
    public function createTestUser($overrides = [])
    {
        $defaults = [
            'name' => 'Test User',
            'email' => ApiHelper::generateRandomEmail(),
            'password' => password_hash('password123', PASSWORD_DEFAULT),
            'status' => 1,
            'created_at' => date('Y-m-d H:i:s')
        ];
        $data = array_merge($defaults, $overrides);
        $sql = "INSERT INTO users (name, email, password, status, created_at) 
                VALUES (:name, :email, :password, :status, :created_at)";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($data);
        return $this->pdo->lastInsertId();
    }
    // 清理测试数据
    public function cleanupTestData($table, $id)
    {
        $sql = "DELETE FROM {$table} WHERE id = :id";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute(['id' => $id]);
    }
    // 重置自增ID
    public function resetAutoIncrement($table)
    {
        $this->pdo->exec("ALTER TABLE {$table} AUTO_INCREMENT = 1");
    }
}

PHPUnit配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml -->
<phpunit bootstrap="vendor/autoload.php"
         colors="true"
         verbose="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="API Test Suite">
            <directory>tests/Api</directory>
        </testsuite>
    </testsuites>
    <groups>
        <exclude>
            <group>slow</group>
        </exclude>
    </groups>
    <php>
        <env name="API_BASE_URL" value="http://localhost:8000/api"/>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_HOST" value="localhost"/>
        <env name="DB_NAME" value="api_test"/>
        <env name="DB_USER" value="root"/>
        <env name="DB_PASS" value=""/>
    </php>
    <logging>
        <log type="junit" target="tests/Report/junit.xml"/>
        <log type="testdox-html" target="tests/Report/testdox.html"/>
        <log type="testdox-text" target="tests/Report/testdox.txt"/>
    </logging>
</phpunit>

数据驱动测试

{
  "test_user_register": {
    "valid_user": {
      "name": "张三",
      "email": "zhangsan@example.com",
      "password": "Password123!",
      "password_confirmation": "Password123!"
    },
    "invalid_email": {
      "name": "李四",
      "email": "invalid-email",
      "password": "Password123!",
      "password_confirmation": "Password123!"
    },
    "weak_password": {
      "name": "王五",
      "email": "wangwu@example.com",
      "password": "123456",
      "password_confirmation": "123456"
    }
  }
}

CI/CD集成脚本

# .gitlab-ci.yml
stages:
  - test
  - report
api_test:
  stage: test
  image: php:8.1-fpm
  services:
    - mysql:5.7
    - redis:6.0
  before_script:
    - apt-get update && apt-get install -y libzip-dev git unzip
    - docker-php-ext-install pdo_mysql zip bcmath
    - pecl install redis && docker-php-ext-enable redis
    - curl -sS https://getcomposer.org/installer | php
    - php composer.phar install
    - cp .env.testing .env
    - php artisan migrate --database=mysql_test
  script:
    - vendor/bin/phpunit --configuration phpunit.xml
      --coverage-text --colors=never
      --log-junit tests/Report/junit.xml
  artifacts:
    paths:
      - tests/Report/
    expire_in: 1 week
  only:
    - develop
    - master
    - /^feature\/.*$/

运行测试

# 运行所有测试
vendor/bin/phpunit
# 运行指定组测试
vendor/bin/phpunit --group=user
# 生成代码覆盖率报告
vendor/bin/phpunit --coverage-html tests/Report/coverage
# 运行单个测试文件
vendor/bin/phpunit tests/Api/User/UserLoginTest.php
# 使用测试数据库(需先创建)
php artisan migrate --database=mysql_test
php artisan db:seed --database=mysql_test --class=TestDatabaseSeeder

最佳实践建议

  1. 测试数据独立:每个测试用例创建自己的数据,测试完成后清理
  2. 环境隔离:使用独立的测试数据库和配置文件
  3. 幂等性:测试用例多次运行结果一致
  4. 结果验证:不仅验证状态码,还要验证响应体结构和内容
  5. 异常测试:覆盖各种边界条件和错误场景
  6. 性能考虑:对慢查询和大量数据测试谨慎处理

这个框架提供了完整的PHP接口自动化测试解决方案,可以根据具体项目需求进行调整和扩展。

抱歉,评论功能暂时关闭!