测试指南

April 30, 2026 · View on GitHub

本文档介绍 PHPX 项目的测试框架和测试方法。

测试目录结构

tests/
├── src/              # C++ 单元测试
│   ├── main.cpp      # 测试入口
│   ├── base.cpp      # 基础功能测试
│   ├── variant.cpp   # Variant 类型测试
│   ├── array.cpp     # Array 类型测试
│   ├── object.cpp    # Object 类型测试
│   └── ...           # 其他测试
├── ext/              # 扩展测试
│   ├── main.cpp      # 扩展示例
│   └── ...           # 扩展功能测试
├── include/          # 测试辅助文件
│   └── phpx_test.h   # 测试头文件
├── unit/             # 单元测试
└── bootstrap.php     # PHP 测试引导

测试框架

Google Test

PHPX 使用 Google Test (GTest) 作为 C++ 单元测试框架。

安装 GTest

# Ubuntu/Debian
sudo apt-get install libgtest-dev cmake
cd /usr/src/gtest
sudo cmake CMakeLists.txt
sudo make
sudo cp *.a /usr/lib

# macOS
brew install googletest

验证安装

php-config --includes  # 确保包含 GTest 路径

运行测试

编译测试程序

标准编译

cd /home/swoole/workspace/projects/phpx
cmake .
make phpx-tests -j 4

Debug 模式编译(推荐用于排查问题)

# 清理之前的构建
cmake --build . --target clean

# 配置 Debug 模式
cmake -DCMAKE_BUILD_TYPE=Debug .

# 编译(包含调试符号和运行时检查)
make phpx-tests -j 4

Debug 模式特性:

  • ✅ 生成完整的调试符号(.pdb 文件在 Windows 上)
  • ✅ 禁用编译器优化,便于单步调试
  • ✅ 启用运行时错误检查(Windows: /RTC1)
  • ✅ 显示详细的头文件包含信息(Windows: /showIncludes)
  • ✅ 更容易定位崩溃和内存问题

执行测试

# 运行所有测试
./bin/phpx-tests

# 运行特定测试套件
./bin/phpx-tests --gtest_filter="base.*"
./bin/phpx-tests --gtest_filter="variant.*"
./bin/phpx-tests --gtest_filter="array.*"

# 运行单个测试
./bin/phpx-tests --gtest_filter="base.echo"

# 列出所有测试
./bin/phpx-tests --gtest_list_tests

测试输出格式

# 默认输出
./bin/phpx-tests

# 详细输出
./bin/phpx-tests --gtest_print_time=1

# XML 输出(用于 CI)
./bin/phpx-tests --gtest_output=xml:test_results.xml

测试代码结构

测试宏定义

#include "phpx_test.h"
#include "phpx_func.h"

using namespace php;

// 测试套件定义
TEST(SuiteName, TestCaseName) {
    // 测试代码
}

// 带参数的测试
TEST_P(SuiteName, TestCaseName) {
    // 参数化测试代码
}

// 测试夹具
class SuiteName : public ::testing::Test {
protected:
    void SetUp() override {
        // 每个测试前执行
    }
    
    void TearDown() override {
        // 每个测试后执行
    }
};

断言宏

// 布尔断言
ASSERT_TRUE(condition);
ASSERT_FALSE(condition);

// 相等断言
ASSERT_EQ(expected, actual);
ASSERT_NE(val1, val2);
ASSERT_LT(val1, val2);
ASSERT_LE(val1, val2);
ASSERT_GT(val1, val2);
ASSERT_GE(val1, val2);

// 字符串断言
ASSERT_STREQ(str1, str2);
ASSERT_STRCASEEQ(str1, str2);
ASSERT_STRNE(str1, str2);
ASSERT_STRCASENE(str1, str2);

// 浮点数断言(考虑精度)
ASSERT_FLOAT_EQ(expected, actual);
ASSERT_DOUBLE_EQ(expected, actual);
ASSERT_NEAR(val1, val2, abs_error);

// 异常断言
ASSERT_THROW(statement, exception_type);
ASSERT_NO_THROW(statement);
ASSERT_ANY_THROW(statement);

// 自定义失败
FAIL() << "Error message";
ADD_FAILURE() << "Warning message";

测试示例

1. 基础测试 (base.cpp)

#include "phpx_test.h"
#include "phpx_func.h"

using namespace php;

// 测试错误输出
TEST(base, error) {
    error(E_WARNING, "php error: %s, ErrorCode: %d", "hello world", 1001);
}

// 测试常量获取
TEST(base, constant) {
    auto c = constant("PHP_VERSION");
    ASSERT_TRUE(c.isString());
    ASSERT_GT(c.length(), 3);
    ASSERT_STREQ(c.toCString(), PHP_VERSION);
}

// 测试多个常量操作
TEST(base, constant2) {
    // 定义常量
    define("IA", 6492);
    auto c = constant("IA");
    ASSERT_TRUE(c.isInt());
    ASSERT_EQ(c.toInt(), 6492);

    // 获取类常量(不存在)
    auto c3 = constant("DateTime", "XXTT2");
    ASSERT_TRUE(c3.isNull());

    // 尝试获取不存在的常量(应抛出异常)
    try_call([]() { 
        auto c4 = constant("XXTT3"); 
    }, "Undefined constant \"XXTT3\"");

    // 获取 PDO 类常量
    auto c4 = constant("PDO", "PARAM_STMT");
    ASSERT_EQ(c4.toInt(), 4);

    // 使用字符串语法
    auto c5 = constant("Pdo::PARAM_STMT");
    ASSERT_EQ(c5.toInt(), 4);

    // 使用类入口
    auto ce = getClassEntrySafe("PDO");
    auto c6 = constant(ce, "PARAM_STMT");
    ASSERT_EQ(c6.toInt(), 4);

    // 获取全局常量
    auto c7 = constant(nullptr, "PHP_VERSION");
    ASSERT_TRUE(c7.isString());
    ASSERT_GT(c7.length(), 3);

    // 获取不存在的类常量
    auto c8 = constant("PDO", "XXTT2");
    ASSERT_TRUE(c8.isNull());
}

// 测试输出函数
TEST(base, echo) {
    // 格式化输出
    echo("php error: %s, ErrorCode: %d\n", "hello world", 1001);

    // 字符串输出
    String s("hello world\n");
    echo(s);

    // 变量输出
    var b = 2026;
    echo(b);
    echo("\n");
    
    echo(1987L);
    echo("\n");
    
    echo(3.1415926);
    echo("\n");
}

// 测试常量定义
TEST(base, define) {
    // 定义字符串常量
    define("test_var1", PHP_VERSION);
    auto c = constant("test_var1");
    ASSERT_TRUE(c.isString());
    ASSERT_GT(c.length(), 0);
    ASSERT_STREQ(c.toCString(), PHP_VERSION);

    // 定义整数常量
    define("test_var2", PHP_VERSION_ID);
    c = constant("test_var2");
    ASSERT_TRUE(c.isInt());
    ASSERT_EQ(c.toInt(), PHP_VERSION_ID);

    // 定义数组常量
    Array array;
    array.set("PHP_MAJOR_VERSION", PHP_MAJOR_VERSION);
    array.set("PHP_MINOR_VERSION", PHP_MINOR_VERSION);
    define("test_var3", array);
    c = constant("test_var3");
    ASSERT_TRUE(c.isArray());
    ASSERT_EQ(c.length(), 2);

    // 验证数组内容
    Array arr2(c);
    ASSERT_EQ(arr2["PHP_MAJOR_VERSION"].toInt(), PHP_MAJOR_VERSION);
}

2. Variant 测试 (variant.cpp)

#include "phpx_test.h"

using namespace php;

// 测试 Variant 构造
TEST(variant, constructor) {
    // 默认构造
    Variant v1;
    ASSERT_TRUE(v1.isNull());
    
    // 整数构造
    Variant v2 = 42;
    ASSERT_TRUE(v2.isInt());
    ASSERT_EQ(v2.toInt(), 42);
    
    // 浮点数构造
    Variant v3 = 3.14;
    ASSERT_TRUE(v3.isFloat());
    ASSERT_FLOAT_EQ(v3.toFloat(), 3.14);
    
    // 字符串构造
    Variant v4 = "hello";
    ASSERT_TRUE(v4.isString());
    ASSERT_STREQ(v4.toCString(), "hello");
    
    // 布尔构造
    Variant v5 = true;
    ASSERT_TRUE(v5.isBool());
    ASSERT_TRUE(v5.toBool());
}

// 测试类型转换
TEST(variant, type_conversion) {
    // 数字转字符串
    Variant num = 123;
    String str = num.toString();
    ASSERT_STREQ(str.data(), "123");
    
    // 字符串转数字
    Variant str_var = "456";
    ASSERT_TRUE(str_var.isNumeric());
    ASSERT_EQ(str_var.toInt(), 456);
    
    // 布尔转换
    Variant zero = 0;
    ASSERT_FALSE(zero.toBool());
    
    Variant non_zero = 100;
    ASSERT_TRUE(non_zero.toBool());
}

// 测试操作符重载
TEST(variant, operators) {
    Variant a = 10;
    Variant b = 3;
    
    // 算术运算
    ASSERT_EQ((a + b).toInt(), 13);
    ASSERT_EQ((a - b).toInt(), 7);
    ASSERT_EQ((a * b).toInt(), 30);
    ASSERT_EQ((a / b).toFloat(), 3.333333);
    ASSERT_EQ((a % b).toInt(), 1);
    
    // 复合赋值
    a += b;
    ASSERT_EQ(a.toInt(), 13);
    
    // 比较运算
    ASSERT_TRUE(a == b);
    ASSERT_TRUE(a > b);
    ASSERT_FALSE(a < b);
}

// 测试引用计数
TEST(variant, reference_counting) {
    Variant v1 = "test";
    ASSERT_EQ(v1.getRefCount(), 1);
    
    // 拷贝构造
    Variant v2 = v1;
    ASSERT_GE(v1.getRefCount(), 1);
    
    // 赋值
    Variant v3;
    v3 = v1;
    ASSERT_GE(v1.getRefCount(), 1);
}

3. Array 测试 (array.cpp)

#include "phpx_test.h"

using namespace php;

// 测试数组创建
TEST(array, creation) {
    // 空数组
    Array arr1;
    ASSERT_EQ(arr1.count(), 0);
    
    // 指定大小
    Array arr2(10);
    ASSERT_GE(arr2.count(), 0);
    
    // 从列表初始化
    Array arr3 = {1, 2, 3, 4, 5};
    ASSERT_EQ(arr3.count(), 5);
}

// 测试元素访问
TEST(array, element_access) {
    Array arr;
    
    // 设置元素
    arr.set("name", "John");
    arr.set("age", 25);
    arr.append("item1");
    arr.append("item2");
    
    // 获取元素
    ASSERT_STREQ(arr["name"].toCString(), "John");
    ASSERT_EQ(arr["age"].toInt(), 25);
    ASSERT_STREQ(arr[0].toCString(), "item1");
    ASSERT_STREQ(arr[1].toCString(), "item2");
    
    // 检查存在性
    ASSERT_TRUE(arr.exists("name"));
    ASSERT_TRUE(arr.exists(0));
    ASSERT_FALSE(arr.exists("not_exists"));
}

// 测试数组操作
TEST(array, operations) {
    Array arr1 = {1, 2, 3};
    Array arr2 = {4, 5, 6};
    
    // 合并
    Array merged = arr1.merge(arr2);
    ASSERT_EQ(merged.count(), 6);
    
    // 推送和弹出
    arr1.push(4);
    ASSERT_EQ(arr1.count(), 4);
    
    Variant popped = arr1.pop();
    ASSERT_EQ(popped.toInt(), 4);
    ASSERT_EQ(arr1.count(), 3);
    
    // 切片
    Array sliced = arr1.slice(1, 2);
    ASSERT_EQ(sliced.count(), 2);
}

// 测试遍历
TEST(array, iteration) {
    Array arr;
    arr.set("a", 1);
    arr.set("b", 2);
    arr.set("c", 3);
    
    std::vector<std::pair<std::string, int>> results;
    
    arr.foreach([&results](Variant &key, Variant &value) {
        results.push_back({key.toStdString(), value.toInt()});
    });
    
    ASSERT_EQ(results.size(), 3);
}

4. Object 测试 (object.cpp)

#include "phpx_test.h"

using namespace php;

// 测试对象创建
TEST(object, creation) {
    // 创建 stdClass
    Object obj = newObject("stdClass");
    ASSERT_STREQ(obj.getClassName().data(), "stdClass");
    
    // 创建 DateTime
    Object date = newObject("DateTime", {"2026-03-27"});
    ASSERT_STREQ(date.getClassName().data(), "DateTime");
}

// 测试属性访问
TEST(object, property_access) {
    Object obj = newObject("stdClass");
    
    // 设置属性
    obj.setProperty("name", "John");
    obj.setProperty("age", 25);
    
    // 获取属性
    ASSERT_STREQ(obj.getProperty("name").toCString(), "John");
    ASSERT_EQ(obj.getProperty("age").toInt(), 25);
    
    // 检查属性
    ASSERT_TRUE(obj.hasProperty("name"));
    ASSERT_FALSE(obj.hasProperty("not_exists"));
}

// 测试方法调用
TEST(object, method_call) {
    Object date = newObject("DateTime", {"2026-03-27 10:30:00"});
    
    // 调用方法
    Variant formatted = date.call("format", {"Y-m-d H:i:s"});
    ASSERT_STREQ(formatted.toCString(), "2026-03-27 10:30:00");
    
    // 调用带返回值的方法
    Variant timestamp = date.call("getTimestamp");
    ASSERT_TRUE(timestamp.isInt());
}

// 测试类型检查
TEST(object, type_checking) {
    Object date = newObject("DateTime");
    
    // instanceOf 检查
    ASSERT_TRUE(date.instanceOf("DateTime"));
    ASSERT_TRUE(date.instanceOf("DateTimeInterface"));
    ASSERT_FALSE(date.instanceOf("stdClass"));
}

5. 辅助函数测试 (helper.cpp)

#include "phpx_test.h"
#include "phpx_helper.h"

using namespace php;

// 测试 INI 配置
TEST(helper, ini_get) {
    auto v = ini_get("post_max_size");
    ASSERT_GE(v.length(), 2);
    ASSERT_GE(v.toInt(), 8);
}

// 测试全局变量
TEST(helper, global) {
    // 获取 _SERVER
    auto server = global("_SERVER");
    ASSERT_TRUE(server.isArray());
    Array array(server);
    ASSERT_TRUE(array.exists("SHELL"));
    
    auto time = array["REQUEST_TIME"];
    ASSERT_TRUE(time.isInt());
    
    // 获取不存在的变量
    auto v2 = global("global_var_not_exists");
    ASSERT_FALSE(v2.toBool());
}

// 测试 eval
TEST(helper, eval) {
    ob_start();
    eval("print_r(PHP_VERSION);");
    auto rs = ob_get_clean();
    ASSERT_TRUE(rs.isString());
    ASSERT_TRUE(str_contains(rs, PHP_VERSION).toBool());
}

// 测试异常捕获
TEST(helper, exception) {
    bool done = false;
    
    try {
        auto e = newObject("RuntimeException", {"phpx error", 1999});
        throwException(e);
    } catch (zend_object *ex) {
        auto e = catchException();
        ASSERT_TRUE(e.getClassName().equals("RuntimeException"));
        done = true;
    }
    
    ASSERT_TRUE(done);
}

编写新测试

步骤

  1. 创建测试文件
touch tests/src/my_feature.cpp
  1. 添加测试代码
#include "phpx_test.h"
#include "phpx_func.h"

using namespace php;

TEST(my_feature, basic_test) {
    // 测试代码
    Variant result = myFunction();
    ASSERT_TRUE(result.isString());
}

TEST(my_feature, advanced_test) {
    // 更复杂的测试
    Array input = {1, 2, 3};
    Array output = processArray(input);
    ASSERT_EQ(output.count(), 6);
}
  1. 添加到 CMakeLists.txt
# tests/src/CMakeLists.txt 或主 CMakeLists.txt
file(GLOB_RECURSE TEST_FILES tests/src/*.cpp)
add_executable(phpx-tests ${TEST_FILES})
  1. 编译并运行
cmake .
make phpx-tests
./bin/phpx-tests --gtest_filter="my_feature.*"

测试模板

#include "phpx_test.h"

using namespace php;

// 测试夹具(可选)
class MyFeatureTest : public ::testing::Test {
protected:
    void SetUp() override {
        // 每个测试前执行
    }
    
    void TearDown() override {
        // 每个测试后执行
    }
};

// 简单测试
TEST(MyFeature, BasicTest) {
    // Arrange
    Variant input = createInput();
    
    // Act
    Variant result = process(input);
    
    // Assert
    ASSERT_TRUE(result.isExpectedType());
    EXPECT_EQ(result.getValue(), expectedValue);
}

// 参数化测试
class MyFeatureParamTest 
    : public MyFeatureTest, 
      public ::testing::WithParamInterface<int> {
};

TEST_P(MyFeatureParamTest, HandlesDifferentInput) {
    int param = GetParam();
    // 使用参数进行测试
    ASSERT_GE(process(param), 0);
}

INSTANTIATE_TEST_SUITE_P(
    MyFeatureTests,
    MyFeatureParamTest,
    ::testing::Values(1, 2, 3, 4, 5)
);

PHP 测试

Bootstrap 文件

tests/bootstrap.php

<?php

declare(strict_types=1);

ini_set('display_errors', 'on');
ini_set('display_startup_errors', 'on');

error_reporting(E_ALL & ~E_DEPRECATED);
date_default_timezone_set('Asia/Shanghai');

!defined('BASE_PATH') && define('BASE_PATH', dirname(__DIR__, 1));

define('EXT_NAME', 'phpx_test');

require BASE_PATH . '/vendor/autoload.php';

运行 PHP 测试

# 使用 PHPUnit
vendor/bin/phpunit tests/

# 使用内置测试工具
php tests/run-tests.php

编写 PHP 测试

<?php

namespace Phpx\Tests;

use PHPUnit\Framework\TestCase;

class ExtensionTest extends TestCase
{
    public function testExtensionLoaded(): void
    {
        $this->assertTrue(extension_loaded('phpx_test'));
    }
    
    public function testFunctionExists(): void
    {
        $this->assertTrue(function_exists('test_function'));
    }
    
    public function testFunctionBehavior(): void
    {
        $result = test_function('input');
        $this->assertEquals('expected', $result);
    }
}

测试覆盖率

生成覆盖率报告

# 启用覆盖率编译
cmake . -DCODE_COVERAGE=ON
make clean
make -j 4

# 运行测试
./bin/phpx-tests

# 生成覆盖率报告
gcov -o CMakeFiles/phpx-tests.dir/tests/src/ src/core/*.cc
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_html

查看覆盖率

# 在浏览器中打开
firefox coverage_html/index.html

持续集成

GitHub Actions

.github/workflows/test.yml

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Install dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y php8.1-dev cmake g++ libgtest-dev
    
    - name: Build
      run: |
        cmake .
        make -j 4
    
    - name: Run tests
      run: |
        ./bin/phpx-tests --gtest_output=xml:test_results.xml
    
    - name: Upload test results
      uses: actions/upload-artifact@v2
      with:
        name: test-results
        path: test_results.xml

调试测试

使用 GDB

# 启动 gdb
gdb ./bin/phpx-tests

# 在 gdb 中运行
(gdb) run --gtest_filter="base.error"

# 如果崩溃,查看堆栈
(gdb) bt

使用 Valgrind

# 检查内存泄漏
valgrind --leak-check=full ./bin/phpx-tests

# 详细输出
valgrind --leak-check=full --show-leak-kinds=all ./bin/phpx-tests

最佳实践

1. 测试命名

// 好的命名
TEST(user_validation, validates_email_format)
TEST(user_validation, rejects_invalid_email)
TEST(user_validation, handles_null_input)

// 避免模糊命名
TEST(test1, test)  // ❌

2. 测试独立性

// ✅ 每个测试独立
TEST(suite, test1) {
    setup();
    // 测试代码
    teardown();
}

TEST(suite, test2) {
    setup();
    // 测试代码
    teardown();
}

// ❌ 测试间依赖
TEST(suite, test1) {
    // 设置状态
}

TEST(suite, test2) {
    // 依赖 test1 的状态
}

3. 测试边界条件

TEST(array_edge_cases, empty_array) {
    Array arr;
    ASSERT_EQ(arr.count(), 0);
}

TEST(array_edge_cases, large_array) {
    Array arr(1000000);
    ASSERT_GE(arr.count(), 0);
}

TEST(array_edge_cases, negative_index) {
    Array arr = {1, 2, 3};
    // 测试负索引行为
}

4. 测试异常

TEST(exception_tests, throws_on_invalid_input) {
    ASSERT_THROW({
        dangerousFunction(invalid_input);
    }, InvalidArgumentException);
}

TEST(exception_tests, no_throw_on_valid_input) {
    ASSERT_NO_THROW({
        safeFunction(valid_input);
    });
}

常见问题

Q: 测试编译失败

# 清理重新编译
make clean
rm -rf CMakeCache.txt CMakeFiles/
cmake .
make phpx-tests

Q: 测试段错误

# 使用 gdb 定位
gdb ./bin/phpx-tests
(gdb) run
(gdb) bt  # 查看崩溃堆栈

Q: 如何跳过某些测试

# 使用过滤器
./bin/phpx-tests --gtest_filter="-slow_test.*"

# 禁用测试
TEST(DISABLED_suite, test) {
    // 这个测试不会被运行
}

本文档最后更新:2026-03-27