重新组织测试文件的选项,其中一个类依赖于另一个类?

发布于 2024-12-03 10:39:29 字数 1673 浏览 0 评论 0原文

我正在重新组织遗留 php 项目的测试用例来测试套件,并遇到了测试依赖于另一个类的类的问题。这是测试文件夹的原始目录布局。

tests/
   |  
   +- models/
   |     | ClassATest.php
   |     | ClassBTest.php
   |     | ...
   +- controllers/
   |     | ...

目前,没有测试套件。每个测试都是单独运行的。

为了重新组织这些测试,首先,我创建 phpunit.xml 并将测试分组到如下所示的套件中。

phpunit.xml

<phpunit bootstrap="...">
    <testsuite name="Models">
        <directory>./models</directory>
    </testsuite>
    ...
</phpunit>

现在我遇到了麻烦。我们有两个模型,ClassA和ClassB,其中ClassB的方法依赖于ClassA。

class ClassA
{
    public function getValue(...)
    {
        ...
    }
}

class ClassB
{
    public function doSomething()
    {
        $a = new ClassA();
        $x = $a->getValue(...);
        // do something with $x
        ...
    }
 }

说到这里,我想大家在测试的时候都看到了这个问题。为了模拟 ClassA::getValue(),ClassBTest.php 已(重新)定义了 ClassA,如下所示。

ClassBTest.php

class ClassA
{
    public function getValue(...)
    {
        return 'a mock value';
    }
}

class ClassBTest extends PHPUnit_Framework_TestCase
{
    public function testDoSomething()
    {
        $b = new ClassB();
        // Make an assertion with $b->doSomething()
    }
}

因此,当我们单独测试每个模型时,它是有效的。但是当在套件中运行时,它会导致错误,因为现在 ClassA 在运行过程的某个时刻被重新声明,并且 phpunit 被终止。

我的问题是如何处理这种情况。我尝试过一些选择。

  • 删除测试文件中重新定义的类并使用依赖注入。这种方式不是首选方式,因为我们不想修改依赖注入的模型类。
  • 使用组注释排除诸如ClassBTest.php之类的测试文件,并单独运行排除的文件。这个选项并不方便。

我们可以配置 phpunit 在一个进程中运行一组文件,在另一个进程中运行另一组文件吗? PHP 可以在运行时动态加载和卸载类吗?如果您还有其他选择,请提出建议。

谢谢!

I'm reorganizing the test cases of a legacy php project to test suites and encounter the problem of testing a class that depends on another class. Here is the original directory layout of the tests folder.

tests/
   |  
   +- models/
   |     | ClassATest.php
   |     | ClassBTest.php
   |     | ...
   +- controllers/
   |     | ...

Currently, there is no test suite. Each test is run separately.

To reorganize these tests, as a start, I create phpunit.xml and group the tests into suites like below.

phpunit.xml

<phpunit bootstrap="...">
    <testsuite name="Models">
        <directory>./models</directory>
    </testsuite>
    ...
</phpunit>

Now I get into trouble. We have two models, ClassA and ClassB, in which a method of ClassB depends on ClassA.

class ClassA
{
    public function getValue(...)
    {
        ...
    }
}

class ClassB
{
    public function doSomething()
    {
        $a = new ClassA();
        $x = $a->getValue(...);
        // do something with $x
        ...
    }
 }

Here, I think we all see the problem when it comes to testing. To mock ClassA::getValue(), ClassBTest.php has (re-)defined ClassA like the following.

ClassBTest.php

class ClassA
{
    public function getValue(...)
    {
        return 'a mock value';
    }
}

class ClassBTest extends PHPUnit_Framework_TestCase
{
    public function testDoSomething()
    {
        $b = new ClassB();
        // Make an assertion with $b->doSomething()
    }
}

So, when we test each model separately, it works. But when run in a suite, it causes an error since now ClassA is re-declared at some point of the running process, and phpunit is terminated.

My question is how to deal with this situation. I have tried some options.

  • Removing the re-defined classes in the test files and using dependency injection. This way is not preferred since we don't want to modify the model classes for dependency injection.
  • Using the group annotation to exclude such test files as ClassBTest.php and running the excluded files separately. This option is not convenient.

Can we configure phpunit to run a group of files in a process and another group of files in another process? Can PHP load and unload a class dynamically at runtime? If you have any other options, please suggest.

Thanks!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

伊面 2024-12-10 10:39:29

PHP 可以在运行时动态加载和卸载类吗?如果您还有其他选择,请提出建议。

是的。这是测试无法重构的代码的最后手段。查看runkit 扩展 method_rename。您可以重命名您想要“模拟”的原始方法。创建您的模拟函数并在测试后将其重命名回来。

较新的 php 测试助手还提供重命名功能。


或者,您可以使用"runInIsolation" / "process-isolation"。每个测试用例都将在另一个 PHP 进程中运行。如果您只在测试用例中定义这些类,您将不会遇到“已定义”的问题。


第三个选项不依赖扩展或生成大量导致测试套件变慢的 php 进程

它可能看起来有点黑客,但只是提供所有选项:

class ClassB
{
    public function doSomething()
    {
        $a = new ClassA();
        $x = $a->getValue(...);
        // do something with $x
        ...
    }
 }

没有太多

class ClassB
{
公共函数 doSomething()
{
$a = $this->getClassA();
$x = $a->getValue(...);
// 用 $x 做一些事情
...

protected function getClassA() {
    if(!$this->classA) { 
        return new ClassA(); 
    } 
    return $this->classA;
}

public function setClassAForTesting(ClassA $classA) {
    $this->classA = $classA;
}

用“测试代码”扰乱您的代码库,但使用setter注入允许您使用依赖项注入进行测试,而您的生产代码将像以前一样工作

这种方法需要做一些工作,但根据我的经验,它比使用 PHP 扩展来破坏行为更具可持续性。

Can PHP load and unload a class dynamically at runtime? If you have any other options, please suggest.

Yes. It's the last resort for testing code you can't refactor. Take a look at the runkit extension, method_rename. You could rename the orignal method you want to "mock". Create your mock function and rename it back after the test.

The newer php test helpers also offers a rename functionality.


Alternatively you can use "runInIsolation" / "process-isolation". Every test case will be run in another PHP process. If you only define those classes withing the test case you won't run into "already defined" issues.


A third option that doesn't rely on extensions or spawing a lot of php process that make your test suite slow

It might seem a bit hackisch but just to offer all options:

class ClassB
{
    public function doSomething()
    {
        $a = new ClassA();
        $x = $a->getValue(...);
        // do something with $x
        ...
    }
 }

without much refactoring you could to the following:

class ClassB
{
public function doSomething()
{
$a = $this->getClassA();
$x = $a->getValue(...);
// do something with $x
...
}

protected function getClassA() {
    if(!$this->classA) { 
        return new ClassA(); 
    } 
    return $this->classA;
}

public function setClassAForTesting(ClassA $classA) {
    $this->classA = $classA;
}

}

This will clutter your code base with "code for testing" but using setter injection allows you to use dependency injection for testing while your production code will work just as before.

This method requires some work but it, in my experience, much more sustainable than using PHP Extensions to hack in behavior.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文