使用嵌套依赖项和Factory类执行单元测试

时间:2022-10-15 20:20:10

I'm new to unit testing and PHPUnit, but I've reading a lot lately about design patterns and isolated tests and I've decided to refactor an application I'm working on to get rid of static classes, singletons, hardcoded dependencies and anything else defined on the global scope, hopefully making it "testable" and not a pain in the ass to mantain in the future, since it is meant to be a long term project.

我是单元测试和PHPUnit的新手,但我最近读了很多关于设计模式和隔离测试的内容,我决定重构一个我正在努力摆脱静态类,单例,硬编码依赖项的应用程序。在全球范围内定义的任何其他内容,希望使其“可测试”,而不是未来保持屁股的痛苦,因为它意味着一个长期项目。

So far I believe I understand the theory behind unit testing, but I was wondering, in a scenario where one delegates handling nested dependencies of objects to a Factory, how should one go about unit testing said Factory, or is it just redundant to test it? And what is the best approach to test that the "chain" of dependencies work well in sync?

到目前为止,我相信我理解单元测试背后的理论,但我想知道,在一个委托处理对象的嵌套依赖关系到一个工厂的场景中,应该如何进行单元测试所述工厂,或者它是多余的来测试它?什么是测试依赖关系“链”同步工作的最佳方法?

Let me illustrate the questions. Suppose you have the following "legacy" code:

让我来说明问题。假设您有以下“遗留”代码:

class House {
    protected $material;
    protected $door;
    protected $knob;

    public function __construct() {
        $this->door = new Door();
        $this->knob = $this->door->getKnob();
        $this->material = "stone";

        echo "House material: ".$this->material . PHP_EOL . "<br/>";
        echo "Door material: ".$this->door->getMaterial() . PHP_EOL . "<br/>";
        echo "Knob material: ".$this->knob->getMaterial() . PHP_EOL . "<br/>";
    }
}

class Door {
    protected $material;
    protected $knob;

    public function __construct() {
        $this->knob = new Knob();
        $this->material = "wood";
    }

    public function getKnob() {
        return $this->knob;
    }

    public function getMaterial () {
        return $this->material;
    }

}

class Knob {
    protected $material;

    public function __construct() {
        $this->material = "metal";
    }

    public function getMaterial () {
        return $this->material;
    }
}

$house = new House();

This is (as far as my understanding goes) bad for unit testing, so we replace the hardcoded dependencies with DI + a Factory class:

这是(据我的理解)单元测试不好,所以我们用DI + Factory类替换硬编码的依赖项:

class House {
    protected $material;
    protected $door;
    protected $knob;

    public function __construct($door) {
        $this->door = $door;
        $this->knob = $this->door->getKnob();
        $this->material = "stone";

        echo "House material: ".$this->material . PHP_EOL . "<br/>";
        echo "Door material: ".$this->door->getMaterial() . PHP_EOL . "<br/>";
        echo "Knob material: ".$this->knob->getMaterial() . PHP_EOL . "<br/>";
    }
}

class Door {
    protected $material;
    protected $knob;

    public function __construct($knob) {
        $this->knob = $knob;
        $this->material = "wood";
    }

    public function getKnob() {
        return $this->knob;
    }

    public function getMaterial () {
        return $this->material;
    }

}

class Knob {
    protected $material;

    public function __construct() {
        $this->material = "metal";
    }

    public function getMaterial () {
        return $this->material;
    }
}

class HouseFactory {
    public function create() {
        $knob = new Knob();
        $door = new Door($knob);
        $house = new House($door);

        return $house;
    }
}

$houseFactory = new HouseFactory();
$house = $houseFactory->create();

Now (and again, as far as I understand) House, Door and Knob can be unit tested with mocked dependencies just fine. But:

现在(并且据我所知),House,Door和Knob可以通过模拟依赖进行单元测试。但:

1) What happens with HouseFactory now?

1)HouseFactory现在会发生什么?

Should one just:

应该只是:

  • Not test it since it doesn't have any application logic worth testing yet and Factories generally stay that way. Assume that if the independ tests for House, Door & Knob pass the Factory should be fine.
  • 不测试它,因为它没有任何值得测试的应用程序逻辑,工厂通常保持这种方式。假设如果House,Door&Knob的独立测试通过工厂应该没问题。

  • Refactor the factory somehow, ie, using functions within the class to get each instance in such a way that one may override these functions via PHPUnit to return mock objects, just in case there is some extra logic in the class that could use some testing in the future.
  • 以某种方式重构工厂,即使用类中的函数以这样的方式获取每个实例,即可以通过PHPUnit覆盖这些函数以返回模拟对象,以防万一在类中有一些可以使用某些测试的额外逻辑未来。

2) Is it feasible to set up tests that rely on several (not mocked) dependencies at once? I understand this is technically not unit testing (integration testing perhaps?) but I guess it's still perfectly doable using PHPUnit? Given the example above, I would like to be able to set up a test that not only tests House, Door, Knob and HouseFactory in isolation, but also the results of the interaction of the real objects with each other, perhaps with some of their functions mocked, such as the ones which deal with data. Is PHPUnit a bad choice for this kind of tests?

2)设置依赖于多个(非模拟)依赖项的测试是否可行?我知道这在技术上不是单元测试(也许是集成测试?)但是我猜它仍然可以使用PHPUnit完美地实现?鉴于上面的例子,我希望能够设置一个测试,不仅可以隔离测试House,Door,Knob和HouseFactory,还可以测试真实物体之间相互作用的结果,也许还有一些模拟的函数,例如处理数据的函数。 PHPUnit对于这种测试来说是一个糟糕的选择吗?

Thanks in advance for your time. I realize some of the assumptions I'm making may not be correct, since I'm obviously not an expert on the matter; corrections are welcome and appreciated.

在此先感谢您的时间。我意识到我所做的一些假设可能不正确,因为我显然不是这方面的专家;我们欢迎并赞赏更正。

2 个解决方案

#1


4  

The factory is just like the new keyword. Do you test the new keyword? No, you test if you can construct a class. But that's independent to the factory itself and part of the unit so already part of your unit tests.

工厂就像新关键字一样。你测试新的关键字吗?不,你测试你是否可以构建一个类。但这是独立于工厂本身和单元的一部分,因此已经是单元测试的一部分。

2) is called integration testing. And you can do that with PHPUnit as well.

2)称为集成测试。你也可以用PHPUnit做到这一点。


Edit - As there was some discussion in comments:

编辑 - 由于评论中有一些讨论:

As far as Unit testing is concerned, you could unit-test your factory that it does for what it is for: return a concrete type, a type or any type at all.

就单元测试而言,您可以对工厂进行单元测试,确定它的用途:返回具体类型,类型或任何类型。

There is nothing wrong with that, however it's normally not necessary as constructors of the returned type(s) are already under unit-tests and well that test is really trivial and just data-checking which smells like integration testing. Also those types which have that type from the factory as dependency (and which are under unit-test as well) will make compilation/execution fail if the dependency can not be provided. So everything the factory is for, is already tested, even from both sides. And if the factory does not get consumed, well then you don't need to test it.

这没有什么不妥,但是通常没有必要,因为返回类型的构造函数已经在单元测试中,并且测试实际上是微不足道的,只是数据检查有点像集成测试。另外那些从工厂那种类型作为依赖关系(并且在单元测试下也是如此)的类型如果无法提供依赖关系,将使编译/执行失败。所以工厂的一切都已经过测试,甚至来自双方。如果工厂没有消耗掉,那么你就不需要进行测试了。

I suggest you create once a factory purely TDD style, so to pre-formulate the use and then you'll get a feeling for this. You might want to test other aspects of your factory class(es), but probably this belongs more into integration than unit testing.

我建议你创造一个纯粹TDD风格的工厂,所以要预先制定使用,然后你就会有这种感觉。您可能希望测试工厂类的其他方面,但这可能更多地属于集成而不是单元测试。

And I don't wanted to create the impression that other of your units should actually have hardcoded calls to the factory create method(s) instead of getting the dependency injected. As you should not use new inside your units, you should not use Factory::create therein either. Similar to new, the class-name (Factory) is hard-encoded, not injected. It is a hidden dependency then. But dependencies should not be hidden; but made visible.

而且我不想让人觉得其他单位实际上应该对工厂创建方法进行硬编码调用,而不是注入依赖项。因为你不应该在你的单位内使用new,所以你也不应该在其中使用Factory :: create。与new类似,类名(Factory)是硬编码的,不是注入的。那是一种隐藏的依赖。但是不应该隐藏依赖关系;但是显而易见。

#2


0  

You can test it with inheritance.

您可以使用继承来测试它。

Just extend House with a FakeHouse for testing, and then check $material, $door and $knob etc. whether they've changed or not after testing.

只需用FakeHouse扩展House进行测试,然后检查$ material,$ door和$ knob等,测试后它们是否已经改变。

#1


4  

The factory is just like the new keyword. Do you test the new keyword? No, you test if you can construct a class. But that's independent to the factory itself and part of the unit so already part of your unit tests.

工厂就像新关键字一样。你测试新的关键字吗?不,你测试你是否可以构建一个类。但这是独立于工厂本身和单元的一部分,因此已经是单元测试的一部分。

2) is called integration testing. And you can do that with PHPUnit as well.

2)称为集成测试。你也可以用PHPUnit做到这一点。


Edit - As there was some discussion in comments:

编辑 - 由于评论中有一些讨论:

As far as Unit testing is concerned, you could unit-test your factory that it does for what it is for: return a concrete type, a type or any type at all.

就单元测试而言,您可以对工厂进行单元测试,确定它的用途:返回具体类型,类型或任何类型。

There is nothing wrong with that, however it's normally not necessary as constructors of the returned type(s) are already under unit-tests and well that test is really trivial and just data-checking which smells like integration testing. Also those types which have that type from the factory as dependency (and which are under unit-test as well) will make compilation/execution fail if the dependency can not be provided. So everything the factory is for, is already tested, even from both sides. And if the factory does not get consumed, well then you don't need to test it.

这没有什么不妥,但是通常没有必要,因为返回类型的构造函数已经在单元测试中,并且测试实际上是微不足道的,只是数据检查有点像集成测试。另外那些从工厂那种类型作为依赖关系(并且在单元测试下也是如此)的类型如果无法提供依赖关系,将使编译/执行失败。所以工厂的一切都已经过测试,甚至来自双方。如果工厂没有消耗掉,那么你就不需要进行测试了。

I suggest you create once a factory purely TDD style, so to pre-formulate the use and then you'll get a feeling for this. You might want to test other aspects of your factory class(es), but probably this belongs more into integration than unit testing.

我建议你创造一个纯粹TDD风格的工厂,所以要预先制定使用,然后你就会有这种感觉。您可能希望测试工厂类的其他方面,但这可能更多地属于集成而不是单元测试。

And I don't wanted to create the impression that other of your units should actually have hardcoded calls to the factory create method(s) instead of getting the dependency injected. As you should not use new inside your units, you should not use Factory::create therein either. Similar to new, the class-name (Factory) is hard-encoded, not injected. It is a hidden dependency then. But dependencies should not be hidden; but made visible.

而且我不想让人觉得其他单位实际上应该对工厂创建方法进行硬编码调用,而不是注入依赖项。因为你不应该在你的单位内使用new,所以你也不应该在其中使用Factory :: create。与new类似,类名(Factory)是硬编码的,不是注入的。那是一种隐藏的依赖。但是不应该隐藏依赖关系;但是显而易见。

#2


0  

You can test it with inheritance.

您可以使用继承来测试它。

Just extend House with a FakeHouse for testing, and then check $material, $door and $knob etc. whether they've changed or not after testing.

只需用FakeHouse扩展House进行测试,然后检查$ material,$ door和$ knob等,测试后它们是否已经改变。