简体   繁体   English

如何测试工厂类?

[英]How to test factory classes?

Given this class: 鉴于此类:

class MyBuilder {
    public function build($param1, $param2) {

        // build dependencies ...

        return new MyClass($dep1, $dep2, $dep3);
    }
}

How can I unit test this class? 我怎样才能对这门课进行单元测试?

Unit-testing it means I want to test its behavior, so I want to test it builds my object with the correct dependencies. 单元测试意味着我想测试它的行为,所以我想测试它用正确的依赖项构建我的对象。 However, the new instruction is hardcoded and I can't mock it. 但是, new指令是硬编码的,我不能嘲笑它。

For now, I've added the name of the class as a parameter (so I can provide the class name of a mock class), but it's ugly: 现在,我已经将类的名称添加为参数(因此我可以提供模拟类的类名),但它很难看:

class MyBuilder {
    public function build($classname, $param1, $param2) {

        // build dependencies ...

        return new $classname($dep1, $dep2, $dep3);
    }
}

Is there a clean solution or design pattern to make my factories testable? 是否有清洁的解决方案或设计模式使我的工厂可测试?

Factories are inherently testable, you are just trying to get too tight of control over the implementation. 工厂本质上是可测试的,你只是试图对实现过于严格控制。

You would check that you get an instance of your class via $this->assertInstanceOf() . 您将通过$this->assertInstanceOf()检查是否获得了类的实例。 Then with the resulting object, you would make sure that properties are set properly. 然后使用生成的对象,确保正确设置属性。 For this you could use any public accessor methods or use $this->assertAttribute* methods that are available in PHPUnit. 为此,您可以使用任何公共访问器方法或使用PHPUnit中提供的$this->assertAttribute*方法。

http://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertEquals http://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.assertions.assertEquals

Many of the common assertions also have the ability to check attributes for protected and private properties. 许多常见断言还能够检查受保护属性和私有属性的属性。

I wouldn't specify the classname in your parameter list, as your usage is that the factory will only return one type and it is only the dependencies that are changed. 我不会在参数列表中指定classname,因为您的用法是工厂只返回一种类型,而且只是更改的依赖项。 Making it return a mock object type is unnecessary and makes your test more complicated. 使其返回模拟对象类型是不必要的,并使您的测试更复杂。

The test would end up looking like this: 测试最终看起来像这样:

public function testBuild() {
    $factory = new MyBuilder();

    //I would likely put the following into a data provider
    $param1 = 'foo';
    $param2 = 'bar';

    $depen1 = 'boo';
    $depen2 = 'baz';
    $depen3 = 'boz';

    $object = $factory->build($param1, $param2);

    $this->assertInstanceOf('MyClass', $object);

    //Check the object definition
    //This would change depending on your actual implementation of your class
    $this->assertAttributeEquals($depen1, 'attr1', $object);
    $this->assertAttributeEquals($depen2, 'attr2', $object);
    $this->assertAttributeEquals($depen3, 'attr3', $object);
}

You are now making sure that your factory returns a proper object. 您现在确保工厂返回正确的对象。 First by making sure that it is of the proper type. 首先要确保它是正确的类型。 Then by making sure that it was initialized properly. 然后确保它已正确初始化。

You are depending upon the existence of MyClass for the test to pass but that is not a bad thing. 你依靠MyClass的存在来测试通过,但这不是一件坏事。 Your factory is intended to created MyClass objects so if that class is undefined then your test should definitely fail. 您的工厂旨在创建MyClass对象,因此如果该类未定义,那么您的测试肯定会失败。

Having failing tests while your developing is also not a bad thing. 在开发过程中进行测试失败也不是一件坏事。

So what do you want to test? 那么你想测试什么?

so I want to test it builds my object with the correct dependencies. 所以我想测试它使用正确的依赖项构建我的对象。

I do see a problem with this. 我确实看到了这个问题。 It's either possible that you can create an object with incorrect dependencies (which should not be the case in the first place or tested in other tests, not with the factory) or you want to test a detail of the factory that you should not test at all. 你有可能创建一个具有不正确依赖关系的对象(首先不应该是这种情况,或者在其他测试中测试,而不是在工厂中测试),或者你想要测试你不应该测试的工厂细节所有。

Otherwise - if it's not mocking the factory what you're looking for - I see no reason why a simple 否则 - 如果它不是在嘲笑工厂你正在寻找什么 - 我认为没有理由为什么这么简单

$actual = $subject->build($param1, $param2);
$this->assertInstanceOf('MyClass', $actual);

would not make it. 不会成功的。 It tests the behavior of the factory build method, that it returns the correct type. 它测试工厂构建方法的行为,它返回正确的类型。

See as well Open-Close-Principle 另见Open-Close-Principle


For tests, you can just create your MockBuilder which extends from your Builder: 对于测试,您可以创建从您的Builder扩展的MockBuilder:

class MyMockBuilder extends MyBuilder {
    public function build($param1, $param2) {

        // build dependencies ...

        return new MyMockClass($dep1, $dep2, $dep3);
    }
}

Making the classname a parameter 1:1 seems not practical to me, because it turns the factory over into something different. 使classname成为参数1:1对我来说似乎不实用,因为它将工厂变成了不同的东西。 The creating is a detail of the factory, nothing you externalize. 创造是工厂的细节,没有你外化。 So it should be encapsulated. 所以它应该被封装。 Hence the MockBuilder for tests. 因此MockBuilder用于测试。 You switch the Factory. 你切换工厂。

As I see it, you ned to verify two things for that builder: 在我看来,你需要为该构建器验证两件事:

  • the correct instance is returned 返回正确的实例
  • values, that are injected are the right ones. 注入的值是正确的。

Checking instance is the easy part. 检查实例很容易。 Verifying values needs a bit of trickery. 验证值需要一些技巧。

The simples way to do this would be altering the autoloader. 这样做的简单方法是改变自动加载器。 You need to make sure that when MyClass is requested for autoloader to fetch, instead of /src/app/myclass.php file it loads /test/app/myclass.php , which actually contains a "transparent" mock (where you with simple getters can verify the values). 你需要确保当我的 MyClass被请求自动加载器获取时,而不是 /src/app/myclass.php文件它加载 /test/app/myclass.php ,它实际上包含一个“透明”模拟(你在那里简单getter可以验证值)。

bad idea 馊主意

Update: 更新:

Also, if you do not want to mess with autoloader, you can just at th top of your myBuilderTest.php file include the mock class file, which contains definition for MyClass . 此外,如果您不想搞乱自动加载器,您可以在myBuilderTest.php文件的顶部包含模拟类文件,其中包含MyClass定义。

... this actually seems like a cleaner way. ......这实际上似乎是一种更清洁的方式。

namespace Foo\Bar;
use PHPUnit_Framework_TestCase;

require TEST_ROOT . '/mocks/myclass.php'

class MyBuilderTest extends PHPUnit_Framework_TestCase
{
    public function MyBuilder_verify_injected_params_test()
    {
         $target = new MyBuilder;
         $instance = $target->build('a', 'b');

         $this->assertEquals('a', $instance->getFirstConstructorParam(); 
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM