返回

PHP Trait 测试:轻松搞定 static 返回类型验证

php

好的,这是你要的博客文章:

PHP Trait 测试难?搞定 static 返回类型验证

咱们在用 PHP 开发的时候,Trait 是个好东西,能帮我们复用代码。但有时候,给 Trait 写测试,尤其是涉及到 static 类型或者闭包的时候,就有点头疼了。

今天就来聊聊这样一个场景:假设你写了个 Trait叫 HasDefault,目的是确保用了这个 Trait 的类,都有个静态的 $default 属性。这个属性是个可执行的回调(callable),调用它就能拿到这个类的默认实例。

<?php

trait HasDefault
{
    private static $default;

    public static function setDefault(callable $fn): void
    {
        // 重点:这里尝试在设置时就检查类型
        self::checkWhetherCallableReturnsClass($fn); 
        static::$default = $fn;
    }

    public static function getDefault(): static
    {
        if (!isset(static::$default)) {
            static::initDefault();
        }
        // 实际返回实例的地方
        return call_user_func(static::$default); 
    }

    protected static function initDefault(): void
    {
        // 默认的初始化逻辑
        static::$default = function (): static {
            return new static();
        };
    }

    /**
     * 检查回调的返回类型是否符合要求 (核心痛点)
     * 理论上需要确保返回的是 `static` 或使用 Trait 的类本身,
     * 或者 `static&SomeInterface` 这种交叉类型。
     */
    private static function checkWhetherCallableReturnsClass(callable $fn): void
    {
        // 实现细节:通常用 Reflection 来检查 $fn 的返回类型提示
        // 但这里问题就来了... 闭包怎么查?匿名类的名字是什么?
        
        // 示例性的伪代码,实际实现会复杂得多
        try {
            if ($fn instanceof \Closure) {
                 // 对于闭包,PHP 7.x 很难静态获取返回类型
                 // PHP 8+ 可以用 ReflectionFunction 获取,但匿名类的名称问题仍在
                 $reflectionFunc = new \ReflectionFunction($fn);
                 $returnType = $reflectionFunc->getReturnType();
                 // 后续需要复杂的逻辑判断 returnType 是否符合 static 的要求
                 // ... 这部分非常棘手,尤其是对于匿名类 ...
                 // throw new \InvalidArgumentException("Callable return type for Closure is invalid.");

            } elseif (is_array($fn) && count($fn) === 2) { // like [$object, 'method'] or [ClassName::class, 'method']
                 $reflectionMethod = new \ReflectionMethod($fn[0], $fn[1]);
                 $returnType = $reflectionMethod->getReturnType();
                 // 同样需要复杂逻辑判断 returnType 是否是 static, self, 或具体的类名
                 // 并且要和 `static::class` (使用 Trait 的类) 进行比较
                 // ... 这也很复杂 ...
                 // throw new \InvalidArgumentException("Callable return type for method is invalid.");
            } 
            // ... 其他 callable 类型 ...

            // 理想的检查逻辑(非常简化):
            // 1. 获取 callable 的 Reflection 对象 (Function or Method)
            // 2. 获取其 ReturnType (ReflectionType)
            // 3. 判断 ReturnType:
            //    - 是 ReflectionNamedType 吗?
            //        - 名字是 'static'? -> OK
            //        - 名字是 'self'? -> OK (在 Trait 定义的上下文中)
            //        - 名字是 `get_called_class()` (实际使用 Trait 的类)? -> OK
            //    - 是 ReflectionIntersectionType 吗?
            //        - 包含 'static' 或 'self' 或 `get_called_class()`? -> OK
            //    - 其他情况 -> Error
            // 4. 如果是闭包且没有类型提示 -> Error? 或者跳过检查?

            // 这个检查非常难完美实现,且可能过度设计。

        } catch (\ReflectionException $e) {
             throw new \InvalidArgumentException("Cannot reflect callable: " . $e->getMessage());
        }
    }
}

核心要求是:$default 回调返回的对象,必须是 使用了这个 Trait 的那个类 的实例,或者是它的子类实例。就像上面代码里 getDefault() 的返回类型 static 所表明的那样。

问题来了:我们想更“主动”一点,不等到调用 getDefault() 的时候才发现类型不对,而是希望在调用 setDefault() 设置回调函数的时候,就提前检查。所以加了个 checkWhetherCallableReturnsClass 私有方法,打算用 PHP 的反射(Reflection)来检查回调的返回类型标记。

这个检查逻辑期望支持以下返回类型:

  1. 使用 Trait 的类的 FQCN (完全限定类名)。
  2. staticself(前提是它们最终解析为使用 Trait 的类)。
  3. 交叉类型,比如 static&SomeInterface,同样 static 需要符合上面的要求。

测试的麻烦在哪?

一般测这种东西,用匿名类挺方便的。比如这样搞一个:

<?php

use PHPUnit\Framework\TestCase;

// 把 Trait 代码放在这里或 require 进来
trait HasDefault { /* ... 上面的 Trait 代码 ... */ }

class HasDefaultTraitTest extends TestCase
{
    private function getObjectThatUsesTrait(): object
    {
        // 创建一个使用了 HasDefault Trait 的匿名类的实例
        return new class(0) { 
            use HasDefault;

            // 可以重写 Trait 的方法,或者添加自己的逻辑
            protected static function initDefault(): void
            {
                static::$default = function (): self { // 这里用 self 指代这个匿名类自身
                    return new self(5); // 返回一个带有 ID 的新实例
                };
            }

            // 用构造函数区分不同实例
            public function __construct(public int $id) 
            {
            }

            // 测试用的方法:返回 $this
            public function returnThisByStatic(): static 
            {
                return $this;
            }

            public function returnThisBySelf(): self 
            {
                return $this;
            }

            // 测试用的方法:返回新实例
            public static function returnsNewByStatic(): static 
            {
                return new static(1); 
            }

            public static function returnsNewBySelf(): self 
            {
                return new self(2);
            }
        };
    }

    // 测试各种有效的方法引用作为回调
    public function testSetDefaultWithValidMethodReferences(): void
    {
        // 测试实例方法 (返回 $this)
        $objectInstance1 = $this->getObjectThatUsesTrait();
        $objectInstance1::setDefault([$objectInstance1, 'returnThisByStatic']); 
        $this->assertSame($objectInstance1, $objectInstance1::getDefault(), 'Test with instance method returning static');
        $this->assertEquals(0, $objectInstance1::getDefault()->id);


        $objectInstance2 = $this->getObjectThatUsesTrait();
        $objectInstance2::setDefault([$objectInstance2, 'returnThisBySelf']);
        $this->assertSame($objectInstance2, $objectInstance2::getDefault(), 'Test with instance method returning self');
        $this->assertEquals(0, $objectInstance2::getDefault()->id);

        // 测试静态方法 (返回新实例)
        $objectClass3 = get_class($this->getObjectThatUsesTrait()); // 获取匿名类的类名
        $objectClass3::setDefault([$objectClass3, 'returnsNewByStatic']);
        $defaultInstance3 = $objectClass3::getDefault();
        $this->assertInstanceOf($objectClass3, $defaultInstance3);
        $this->assertEquals(1, $defaultInstance3->id, 'Test with static method returning static');


        $objectClass4 = get_class($this->getObjectThatUsesTrait());
        $objectClass4::setDefault([$objectClass4, 'returnsNewBySelf']);
        $defaultInstance4 = $objectClass4::getDefault();
        $this->assertInstanceOf($objectClass4, $defaultInstance4);
        $this->assertEquals(2, $defaultInstance4->id, 'Test with static method returning self');
    }
    
    // ... 其他测试 ...
}

用上面 getObjectThatUsesTrait 创建的对象和类,再写几个测试用例,比如:

// (接上面 Test Case)

public function testDefaultInitialization(): void
{
    $object = $this->getObjectThatUsesTrait();
    // 不调用 setDefault,直接 getDefault,应该触发 initDefault
    $defaultInstance = $object::getDefault();
    $this->assertInstanceOf(get_class($object), $defaultInstance);
    $this->assertEquals(5, $defaultInstance->id, 'Test default initDefault logic');
}

// ... 更多测试 ...

上面这些,测试起来都没啥问题。因为 returnThisByStatic, returnsNewBySelf 这些方法都有明确的返回类型 staticselfcheckWhetherCallableReturnsClass 通过反射能拿到信息(理论上,如果 checkWhetherCallableReturnsClass 实现了那复杂的逻辑)。

真正的坑来了:怎么测闭包(Closure)?

public function testSetDefaultWithClosure(): void
{
    $object = $this->getObjectThatUsesTrait();
    $objectInstanceToReturn = $this->getObjectThatUsesTrait(); // 假设我们需要返回一个特定的实例

    // 这个闭包没有返回类型提示!
    $object::setDefault(function () use ($objectInstanceToReturn) { 
        return $objectInstanceToReturn;
    });

    // 这个断言能过,因为 getDefault 实际执行了闭包
    $this->assertSame($objectInstanceToReturn, $object::getDefault()); 
    
    // 但是,如果 checkWhetherCallableReturnsClass 被严格执行,
    // 它很可能在 setDefault 时就因为无法确定闭包的返回类型而抛出异常。
    // 如果允许无类型提示的闭包,那这个检查还有意义吗?
}

public function testSetDefaultWithClosureAndPhp8ReturnType(): void
{
    // 仅适用于 PHP 8.0+
    if (version_compare(PHP_VERSION, '8.0.0', '<')) {
        $this->markTestSkipped('Closure return types are available in PHP 8.0+');
    }

    $object = $this->getObjectThatUsesTrait();
    $theClass = get_class($object); // 获取匿名类的名称

    // 关键点:PHP 8+ 可以在闭包上添加返回类型!
    // 但这里又卡住了:匿名类的名字是动态生成的(类似 "class@anonymous..."),
    // 你没法直接写在代码里作为返回类型提示。
    // $object::setDefault(function() use ($object): class@anonymous... { return $object; }); // 这样写语法错误

    // 如果闭包返回的是 `static``self`,可能还有戏,
    // 但这取决于 `checkWhetherCallableReturnsClass` 如何解析它们。
    // 对于 `static`,PHP 8 的 ReflectionFunction 可以识别。
    try {
        eval("
            \$object = \$this->getObjectThatUsesTrait();
            \$theClass = get_class(\$object);
            \$object::setDefault(function() use (\$object): static { return \$object; });
        "); // 使用 eval 是为了动态插入 static 类型提示,这在实际测试中非常不推荐!

         $this->assertSame($object, $object::getDefault());

    } catch (\Throwable $e) {
        // 如果 checkWhetherCallableReturnsClass 严格执行,或者环境不支持,可能会失败
        $this->fail("Failed to set default with Closure having 'static' return type: " . $e->getMessage());
    }
     
    // 小结:即便 PHP 8 支持了闭包类型提示,配合匿名类,测试 `setDefault` 的检查逻辑依然很别扭。
}

麻烦就在于:

  1. 闭包没类型提示 :上面那个闭包 function () use ($objectInstanceToReturn) { ... },根本没写返回类型。PHP 7.x 的反射拿不到它的返回类型信息,checkWhetherCallableReturnsClass 检查个啥?直接就该报错(如果严格检查的话),或者干脆跳过检查,那这个检查不就形同虚设了?
  2. 匿名类名字未知 :就算用 PHP 8+ 的带类型提示的闭包,比如 function(): XXX { ... },那这个 XXX 该写啥?匿名类的名字是动态生成的,类似 class@anonymous...,你总不能硬编码这个吧?写 staticself 也许行,但这又回到了 checkWhetherCallableReturnsClass 如何解释 static/self 的问题上。
  3. 测试继承 :我们还希望测试当一个类继承了另一个使用 HasDefault 的类时,行为是否正确。但匿名类是 final 的,没法继承。想测继承,就不能用匿名类。
  4. PHPUnit 的 Mock 工具 :你可能想到用 PHPUnit 自带的 Trait 测试工具,比如 $this->getMockForTrait()$this->getObjectForTrait()。可惜,哥们儿,这些方法已经被官方标记为 deprecated 了,并且在 PHPUnit 10 中移除了,官方建议你直接用具体类或匿名类来测试 Trait。详见:https://github.com/sebastianbergmann/phpunit/issues/5244

兜了一圈,不仅 checkWhetherCallableReturnsClass 本身实现起来困难重重,连测试这个检查逻辑都这么费劲。这时候就得琢磨了:费这么大劲在 setDefault 里做这个预检查,到底值不值?我是不是钻牛角尖了?

解决方案有哪些?

别急,路不止一条。咱们看看有哪些搞法。

方案一:把检查重心放在 getDefault()(运行时检查)

这是最直接,也可能是最实用的方法。

  • 原理: 不再纠结于在 setDefault() 时用反射做静态分析。让 setDefault() 只管设置回调。核心的类型保证交给 getDefault()static 返回类型声明。PHP 引擎本身会在 getDefault() 返回时检查类型是否匹配 static(即调用时的类)。如果回调返回了错误类型的对象,PHP 会在 getDefault() 这里抛出 TypeError。你的测试只需要验证 getDefault() 的行为即可。

  • 优点:

    • HasDefault Trait 变简单了,去掉了那个复杂还不好测的 checkWhetherCallableReturnsClass 方法。
    • 测试变简单了,只用关心 getDefault() 是否返回了预期的、类型正确的对象。测试覆盖了实际运行时的行为。
    • 避免了反射带来的复杂性和潜在的性能开销(尽管在 setDefault 里影响不大)。
  • 缺点:

    • 类型错误要等到 getDefault() 被调用时才发现,而不是在设置回调时。但在单元测试里,这个问题不大,因为你肯定会测 getDefault()
  • 怎么做:

    1. 简化 Trait:

      <?php 
      trait HasDefaultSimplified
      {
          private static $default;
      
          // 直接设置,不检查了
          public static function setDefault(callable $fn): void
          {
              static::$default = $fn;
          }
      
          // 依赖 PHP 的 static 返回类型检查
          public static function getDefault(): static 
          {
              if (!isset(static::$default)) {
                  static::initDefault(); 
              }
              // PHP 会在这里检查 call_user_func 的结果是否符合 static
              return call_user_func(static::$default); 
          }
      
          protected static function initDefault(): void
          {
               static::$default = function (): static {
                   return new static();
               };
          }
      }
      
    2. 调整测试: 测试重点放在验证 getDefault() 的返回值和类型。

      <?php
      
      use PHPUnit\Framework\TestCase;
      
      // 使用简化后的 Trait
      trait HasDefaultSimplified { /* ... 上面的简化版 Trait 代码 ... */ }
      
      class HasDefaultSimplifiedTraitTest extends TestCase
      {
          private function getObjectThatUsesTrait(): object
          {
              return new class('default_id') { // 用 id 区分
                  use HasDefaultSimplified; // 注意:用了简化版 Trait
      
                  public function __construct(public string $id) {}
      
                  public static function createStatic(): static { return new static('created_static'); }
                  public function createSelf(): self { return new self('created_self'); }
              };
          }
      
          // 测试有效的设置
          public function testGetDefaultReturnsCorrectType(): void
          {
              $object = $this->getObjectThatUsesTrait();
              $objectClass = get_class($object);
      
              // 1. 测试默认初始化
              $defaultInstance = $objectClass::getDefault();
              $this->assertInstanceOf($objectClass, $defaultInstance);
              // 假设默认 initDefault 创建的对象没有特殊 ID,或者你可以给它设一个
              // $this->assertEquals('default_init_id', $defaultInstance->id); 
      
              // 2. 测试设置静态方法
              $objectClass::setDefault([$objectClass, 'createStatic']);
              $instanceFromStatic = $objectClass::getDefault();
              $this->assertInstanceOf($objectClass, $instanceFromStatic);
              $this->assertEquals('created_static', $instanceFromStatic->id);
      
              // 3. 测试设置实例方法 (需要一个实例来调用)
              $objectInstance = new $objectClass('instance_id');
               $objectClass::setDefault([$objectInstance, 'createSelf']);
               $instanceFromSelf = $objectClass::getDefault();
               $this->assertInstanceOf($objectClass, $instanceFromSelf);
               $this->assertEquals('created_self', $instanceFromSelf->id);
      
              // 4. 测试设置闭包
               $specificInstance = new $objectClass('specific_closure_instance');
               $objectClass::setDefault(function() use ($specificInstance): object { // 返回类型写 object 或省略都行
                   return $specificInstance;
               });
               $instanceFromClosure = $objectClass::getDefault();
               $this->assertInstanceOf($objectClass, $instanceFromClosure); // PHP 的 static 检查通过
               $this->assertSame($specificInstance, $instanceFromClosure); 
          }
      
          // 测试设置无效的回调 (可选,但推荐)
          public function testGetDefaultThrowsErrorForInvalidType(): void
          {
               $object = $this->getObjectThatUsesTrait();
               $objectClass = get_class($object);
      
               // 设置一个返回不相关对象的闭包
               $objectClass::setDefault(function(): \stdClass {
                   return new \stdClass();
               });
      
               // 期望调用 getDefault 时 PHP 抛出 TypeError
               $this->expectException(\TypeError::class);
               // 可以更精确地匹配错误消息,如果需要的话
               // $this->expectExceptionMessageMatches('/Return value must be of type static/'); 
      
               $objectClass::getDefault(); 
          }
      }
      
      
  • 安全建议: 无特殊安全建议,依赖 PHP 内建类型检查是标准做法。

方案二:弃用匿名类,拥抱具体测试类

如果你确实需要测试继承,或者觉得匿名类写起来别扭,那就老老实实定义几个专门用于测试的类。

  • 原理: 在你的测试文件里,或者测试的 _support 目录下,定义几个具体的类。一个基类使用 HasDefault Trait,再来个子类继承它。这样你就有明确的类名了,可以在闭包或者方法签名里写清楚返回类型。

  • 优点:

    • 类名明确,解决了匿名类在类型提示上的尴尬。
    • 可以轻松测试继承场景。
    • 代码结构更清晰,特别是测试复杂交互时。
  • 缺点:

    • 需要写更多“辅助”代码(定义测试类)。
  • 怎么做:

    1. 定义测试类:
      <?php
      
      // --- 放在测试文件顶部,或者单独的辅助文件里 ---
      
      // 基类
      class BaseTestClassForHasDefault 
      {
          use HasDefault; // 或者用简化版的 HasDefaultSimplified
          public function __construct(public string $id = 'base_default') {}
      
          // 示例方法
          public static function makeStatic(): static { return new static('base_static'); }
          public function makeSelf(): self { return new self('base_self'); }
      
           // 可以覆盖 Trait 的 initDefault
           protected static function initDefault(): void
           {
               static::$default = fn(): static => new static('base_init');
           }
      }
      
      // 子类
      class ChildTestClassForHasDefault extends BaseTestClassForHasDefault
      {
          public function __construct(public string $id = 'child_default') { parent::__construct($id); }
      
          // 子类自己的方法
          public static function makeChildStatic(): static { return new static('child_static'); }
          public function makeChildOnly(): self { return new self('child_only'); }
      
          // 覆盖父类的 initDefault
          protected static function initDefault(): void
           {
               static::$default = fn(): static => new static('child_init');
           }
      }
      
      // 一个不相关的类,用于测试错误情况
      class UnrelatedClass {}
      
      // --- 测试 Case ---
      use PHPUnit\Framework\TestCase;
      
      class HasDefaultWithConcreteClassesTest extends TestCase
      {
          // 使用 BaseTestClassForHasDefault 进行测试
          public function testBaseClassBehavior(): void
          {
               // 默认初始化
               $defaultBase = BaseTestClassForHasDefault::getDefault();
               $this->assertInstanceOf(BaseTestClassForHasDefault::class, $defaultBase);
               $this->assertEquals('base_init', $defaultBase->id); // 检查是否调用了 Base 的 initDefault
      
               // 设置静态方法
               BaseTestClassForHasDefault::setDefault([BaseTestClassForHasDefault::class, 'makeStatic']);
               $fromStatic = BaseTestClassForHasDefault::getDefault();
               $this->assertInstanceOf(BaseTestClassForHasDefault::class, $fromStatic);
               $this->assertEquals('base_static', $fromStatic->id);
      
               // 设置闭包 (现在可以写返回类型了!)
                if (version_compare(PHP_VERSION, '8.0.0', '>=')) {
                    // PHP 8+ 可以加类型提示
                     BaseTestClassForHasDefault::setDefault(function(): BaseTestClassForHasDefault {
                         return new BaseTestClassForHasDefault('base_closure_php8');
                     });
                } else {
                    // PHP 7.x 只能这样,依赖运行时检查
                    BaseTestClassForHasDefault::setDefault(function() {
                         return new BaseTestClassForHasDefault('base_closure_php7');
                     });
                }
                $fromClosure = BaseTestClassForHasDefault::getDefault();
                $this->assertInstanceOf(BaseTestClassForHasDefault::class, $fromClosure);
                $this->assertStringContainsString('base_closure', $fromClosure->id);
          }
      
           // 使用 ChildTestClassForHasDefault 测试继承和 static 解析
           public function testChildClassBehavior(): void
           {
                // 默认初始化 (应调用 Child 的 initDefault)
                $defaultChild = ChildTestClassForHasDefault::getDefault();
                $this->assertInstanceOf(ChildTestClassForHasDefault::class, $defaultChild);
                $this->assertEquals('child_init', $defaultChild->id);
      
                // 设置父类的静态方法 (返回的是 Child 实例)
                ChildTestClassForHasDefault::setDefault([ChildTestClassForHasDefault::class, 'makeStatic']);
                $fromParentStatic = ChildTestClassForHasDefault::getDefault();
                // 因为 makeStatic 返回 `static`, 在 Child 上下文调用,应该返回 Child 实例
                $this->assertInstanceOf(ChildTestClassForHasDefault::class, $fromParentStatic); 
                $this->assertEquals('base_static', $fromParentStatic->id); // ID 来自父类方法
      
                // 设置子类的静态方法
                ChildTestClassForHasDefault::setDefault([ChildTestClassForHasDefault::class, 'makeChildStatic']);
                $fromChildStatic = ChildTestClassForHasDefault::getDefault();
                $this->assertInstanceOf(ChildTestClassForHasDefault::class, $fromChildStatic);
                $this->assertEquals('child_static', $fromChildStatic->id);
      
                 // 设置返回父类实例的闭包 (应该在 getDefault 时失败)
                 ChildTestClassForHasDefault::setDefault(function(): BaseTestClassForHasDefault {
                      return new BaseTestClassForHasDefault('explicit_base_instance');
                 });
                 $this->expectException(\TypeError::class);
                 ChildTestClassForHasDefault::getDefault(); // 这里会抛异常,因为 Base 不是 Child
           }
      
           // 测试设置完全错误的类型
           public function testSettingInvalidTypeCallback(): void
           {
               BaseTestClassForHasDefault::setDefault(function(): UnrelatedClass {
                   return new UnrelatedClass();
               });
      
               $this->expectException(\TypeError::class);
               BaseTestClassForHasDefault::getDefault();
           }
      }
      
      

方案三:重新审视 checkWhetherCallableReturnsClass 的必要性

这是最高层次的思考:我们真的需要这个 setDefault 里的预检查吗?

  • 论点:

    • getDefault(): static 已经提供了运行时的强类型保证。PHP 底层会做检查。
    • setDefault 里用反射模拟这个检查,实现起来复杂、易出错,还难测试,尤其对闭包和匿名类。属于是“吃力不讨好”。
    • 这个检查逻辑是否真的增加了代码的健壮性,足以抵消它带来的复杂度和维护成本?多数情况下可能未必。如果一个开发者传了个明显错误的回调,比如 fn() => new \DateTime(),难道不是应该在 getDefault() 时快速失败更直接吗?
    • 单元测试应该关注的是 Trait 的 对外可观察行为 (getDefault 是否按预期工作),而不是其内部实现的细枝末节(比如那个私有的检查方法)。过于关注内部实现细节,会让测试变得脆弱,一旦内部实现微调(即使不影响外部行为),测试就可能失败。
  • 建议: 优先采用方案一 。简化 Trait,移除 checkWhetherCallableReturnsClass,依赖 getDefault(): static 和相应的单元测试来保证正确性。这通常是投入产出比最高的选择。

  • 如果非要提前检查: 如果有特殊场景(比如框架层面的强制约束)真的必须在 setDefault 做点什么,可以考虑:

    • 简化检查目标: 不追求完美的 static 解析,只检查比较明确的情况,比如返回类型是 self 或者具体的类名。对闭包或无法解析的情况,要么信任开发者,要么干脆禁止使用这些形式的回调。
    • 文档约束: 在文档(PHPDoc)里明确写清楚对回调函数的要求,依靠开发者自觉和代码审查。

选哪个?

  • 追求简单实用、相信 PHP 类型系统和运行时检查? -> 方案一 。删掉 checkWhetherCallableReturnsClass,简化 Trait,把测试重心放在 getDefault() 上。这是最推荐的做法。
  • 非常需要测试继承、不介意写辅助代码? -> 方案二 。用具体的测试类替代匿名类。可以配合方案一的简化 Trait,也可以保留(并实现)checkWhetherCallableReturnsClass,因为此时你有明确的类名,反射检查稍微容易一点(但依然复杂)。
  • 觉得预检查逻辑过度设计、投入产出比低? -> 方案三 。这是一个思路上的转变,让你回归到测试的核心目标——验证行为,而不是实现细节。这通常会引导你走向方案一。

总而言之,对于测试 PHP Trait 中涉及 static 和 callable 的复杂场景,特别是像 HasDefault 这样依赖回调返回特定类型实例的情况,过度依赖在设置时进行复杂的静态反射检查,往往会把简单问题复杂化,并且测试起来非常困难。退一步,依赖 PHP 自身的 static 返回类型检查和充分的 getDefault() 行为测试,通常是更简洁、更健壮、也更符合单元测试原则的做法。