在多个命名空间中定义相同的 PHP 函数

Define the same PHP function in many namespaces

提问人:Meyer Auslander - Tst 提问时间:6/15/2023 更新时间:6/15/2023 访问量:52

问:

为了创建用于单元测试的全局函数的模拟版本,我在 SUT 类的命名空间中定义其模拟版本,如此所述

考虑在命名空间和命名空间中使用的全局函数的情况。如果我想在两个地方使用相同的模拟版本,似乎我不得不对foo()BarBazfoo()foo()

/* bar-namespace-mocks.php */
namespace Bar;
function foo(){
   return 'foo mock';
}
/* baz-namespace-mocks.php */
namespace Baz;
function foo(){
   return 'foo mock';
}

此代码不符合 DRY 主体。 最好有一个 foo mock 的声明

/* foo-mock.php */
function foo(){
   return 'foo mock';
}

然后根据需要导入到每个命名空间中,如以下伪代码所示:

/* namespace-mocks.php */
namespace Bar{
  import 'foo-mock.php';
} 
namespace Baz{
  import 'foo-mock.php';
}

使用 include 导入,例如

namespace Baz;
include 'foo-mock.php'

不会导致在命名空间中声明模拟。有没有办法在多个命名空间中声明一个函数,而没有多个版本?Baz

PHP 单元测试 模拟 命名空间

评论


答:

1赞 Alex Howansky 6/15/2023 #1

如果你需要抽象出一个原生函数,那么就为它做一个契约,这样你就可以对它提供的服务使用依赖注入:

interface FooInterface
{
    public function get(): string;
}

然后提供使用本机函数的默认实现:

class NativeFoo implements FooInterface
{
    public function get(): string
    {
        return \foo();
    }
}

然后,您的主类可能如下所示:

class A
{
    protected FooInterface $foo;

    public function __construct(FooInterface $foo = null)
    {
        $this->foo = $foo ?? new NativeFoo();
    }

    public function whatever()
    {
        $foo = $this->foo->get();
    }
}

因此,您在构造函数中有一个可选参数,如果您不提供值,您将获得本机 foo 函数作为默认值。你只需要做:

$a = new A();
$a->whatever();

但是,如果您想覆盖它,您可以执行以下操作:

$foo = new SomeOtherFoo();
$a = new A($foo);
$a->whatever();

然后,在测试中,可以构建一个显式模拟:

class MockFoo implements FooInterface
{
    public function get(): string
    {
        return 'some test value';
    }
}

并传递实例:

$foo = new MockFoo();
$a = new A($foo);
$a->whatever();

或者使用 PHPUnit 模拟构建器:

$foo = $this->getMockBuilder(FooInterface::class)->... // whatever

如果你想要一个更简单的版本,你可以只使用一个可调用的:

class A
{
    protected $foo;

    public function __construct(callable $foo = null)
    {
        $this->foo = $foo ?? 'foo';
    }

    public function whatever()
    {
        $foo = call_user_func($this->foo);
    }
}


// default usage
$a = new A();
$a->whatever();

// test usage
$a = new A(fn() => 'some other value');
$a->whatever();

评论

0赞 Meyer Auslander - Tst 6/15/2023
这在技术上是有效的解决方案。虽然,我更愿意直接调用全局函数而不为其创建包装器。我不想只是为了单元测试而以不寻常的方式编写的代码。foo
1赞 Alex Howansky 6/15/2023
我不会说这是不寻常的,这就是 DI 的工作原理。如果您需要覆盖本机函数,其他机制将很笨拙,并且可能会破坏 PSR-4。也就是说,我已经更新了一个更简单的替代方案。
0赞 Meyer Auslander - Tst 6/15/2023
与其将对象传递到构造函数中,不如直接将受保护成员设置为默认值,这不是更简单吗?然后,对于单元测试,使用反射将其重置为模拟值。$foo
0赞 Alex Howansky 6/15/2023
使用反射?Egads 不,不要那样做。请注意,对于上面的“更简单版本”,没有实例化或传递对象,它只是一个可调用对象,它可以是包含函数名称的静态字符串。例如,你可以做$a = new A('fooOverrideFunction');
0赞 Meyer Auslander - Tst 6/16/2023
在单元测试中使用反射有什么问题?如果是这样,生产代码将更符合 KISS 原则,不需要任何不必要的输入参数。即使我们想将默认值设置为对象,也没问题。但是我们不需要使用 DI 输入参数使类构造函数复杂化。$foo
0赞 Sammitch 6/15/2023 #2

虽然我完全同意 Alex Howansky 的回答,但如果你真的一心想使用简单的函数,那么你总是可以:

namespace Mocks {
    function foo() {
        return 'foo';
    }
}

namespace Bar {
  function foo() {
      return \Mocks\foo();
  }
}

namespace Baz {
  function foo() {
      return \Mocks\foo();
  }
}

namespace asdf {
    var_dump(\Bar\foo(), \Baz\foo());
}

输出:

string(3) "foo"
string(3) "foo"

虽然在这一点上,我们真的只是在重塑类的路上,但更糟。在某些时候,你会想知道如何命名一个变量,这根本不可能,所以你会在这一点上将其滚动到类中。

类、接口和特征/继承将最恰当地利用来解决这个问题。

评论

0赞 Meyer Auslander - Tst 6/16/2023
同意这个解决方案更符合我的要求。更简单地说,我们不需要命名空间。只需将调用的函数放入全局命名空间即可。然后从每个命名空间声明的 's 中调用它。代码似乎仍然重复。但是,至少现在,如果需要进行更新,则只有一个版本要更新。Mocksfoo_mock()foo()foo_mock()