【问题标题】:How to test factory classes?如何测试工厂类?
【发布时间】:2013-11-30 15:47:06
【问题描述】:

鉴于这个类:

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

        // build dependencies ...

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

如何对这个类进行单元测试?

单元测试意味着我想测试它的行为,所以我想测试它以正确的依赖关系构建我的对象。但是,new 指令是硬编码的,我无法模拟它。

目前,我已将类的名称添加为参数(这样我就可以提供模拟类的类名),但这很丑:

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

        // build dependencies ...

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

是否有干净的解决方案或设计模式可以让我的工厂可测试?

【问题讨论】:

    标签: php unit-testing design-patterns testing


    【解决方案1】:

    工厂本质上是可测试的,您只是试图对实现进行过于严格的控制。

    你会检查你是否通过$this->assertInstanceOf() 获得了你的类的实例。然后使用生成的对象,您将确保正确设置属性。为此,您可以使用任何公共访问器方法或使用 PHPUnit 中可用的 $this->assertAttribute* 方法。

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

    许多常见的断言还能够检查受保护和私有属性的属性。

    我不会在您的参数列表中指定类名,因为您的用法是工厂将只返回一种类型,并且仅更改了依赖项。让它返回一个模拟对象类型是不必要的,并且会使您的测试更加复杂。

    测试最终会是这样的:

    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);
    }
    

    您现在正在确保您的工厂返回正确的对象。首先确保它是正确的类型。然后确保它已正确初始化。

    您依赖MyClass 的存在才能通过测试,但这并不是一件坏事。您的工厂旨在创建 MyClass 对象,因此如果该类未定义,那么您的测试肯定会失败。

    在开发过程中测试失败也不是坏事。

    【讨论】:

    • 但是在单元测试中,你不能让它接触到其他单元。如果您正在为工厂类编写测试,则不应在其中创建其他类的对象。依赖关系应该总是被嘲笑。所以这里的问题是你如何模拟你没有从外部传递的东西。
    • @Hafiz 除了工厂的目的是创建其他对象。工厂类的测试确保创建了正确类型的对象。这些对象不是依赖项,它们是函数返回的内容。
    • 是的,明白了,我的错。没有必要模拟,因为它除了创建和返回一个对象之外什么都不做。所以我们只断言类型相等。
    【解决方案2】:

    那么你想测试什么?

    所以我想测试它是否会使用正确的依赖项构建我的对象。

    我确实看到了这个问题。有可能您可以创建一个具有不正确依赖关系的对象(首先不应该是这种情况,或者在其他测试中测试过,而不是在工厂中测试),或者您想要测试不应该测试的工厂细节全部。

    否则 - 如果它不是在模拟您正在寻找的工厂 - 我看不出为什么简单

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

    不会成功。它测试工厂构建方法的行为,它返回正确的类型。

    另见Open-Close-Principle


    对于测试,您可以创建从您的 Builder 扩展而来的 MockBuilder:

    class MyMockBuilder extends MyBuilder {
        public function build($param1, $param2) {
    
            // build dependencies ...
    
            return new MyMockClass($dep1, $dep2, $dep3);
        }
    }
    

    将类名设置为 1:1 的参数对我来说似乎不切实际,因为它将工厂变成了不同的东西。创建是工厂的一个细节,没有任何东西可以外化。所以应该封装。因此,用于测试的 MockBuilder。你切换工厂。

    【讨论】:

    • 好吧,也许在调用工厂的构建方法之后,答案(用 PHPUnit 说话)只是$this->assertInstanceOf('MyClass', $actual);。您要么需要模拟工厂,要么需要测试工厂的创造价值。
    • 我同意将类名作为参数并不好:S。但是我不明白您的解决方案如何让我测试依赖项是否正确?在您的回答中,我(测试人员)将覆盖我要测试的方法。
    • assertInstanceOf不够用,我要测试给MyClass的构造函数的依赖
    • @MatthieuNapoli:测试它们,但仅在每个依赖项的单元测试中。构造函数也不应该做任何实际工作,工厂只需要返回一个(抽象)类型,因此 instanceof 可以很好地工作。我还编辑了答案,因为我觉得您想在错误的区域进行测试,这使得它比需要的更复杂。
    • 我的构建器有帮助方法来设置几个“标志”,build() 返回一个配置的对象。我想检查我的构建器是否确实生成了按照我的要求配置的对象。我觉得没问题,就是黑盒测试。
    【解决方案3】:

    在我看来,您需要为该构建器验证两件事:

    • 返回正确的实例
    • 注入的值是正确的。

    检查实例是简单的部分。验证值需要一些技巧。

    最简单的方法是改变自动加载器。您需要确保当 MyClass 请求自动加载器获取时,而不是 /src/app/myclass.php 文件加载 /test/app/myclass.php,它实际上包含一个“透明”模拟(您可以使用简单的 getter 验证值)。

    坏主意

    更新:

    另外,如果你不想弄乱自动加载器,你可以在 myBuilderTest.php 文件的顶部包含模拟类文件,其中包含 MyClass 的定义。

    ...这实际上似乎是一种更清洁的方式。

    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(); 
        }
    }
    

    【讨论】:

    • 我明白你的意思,但这只是为了测试这个而进行的一些严重的黑客攻击。特别是因为我有一个完整的测试套件并且欺骗自动加载器可能会影响其他测试:(
    • 包含文件 -> 也会弄乱其他测试:S
    • @MatthieuNapoli,为什么它会与其他测试混淆?每个测试类都被单独调用,AFAIK。仅仅因为 MyBuilderTest 类中包含一些文件,并不意味着它会对 'FooBarTest. Also, since you are testing the builder and not the product, then the MyBuilderTest` 产生任何影响,不应包含任何依赖于 MyClass 中逻辑的测试。
    • 因为其他测试也会选择模拟,而不是原始类
    • @MatthieuNapoli ,每个测试类都是独立执行的。无论如何,我怀疑你的问题是通过在MyClass 的构造函数中进行计算引起的,因为如果它只包含分配,那么测试将毫无意义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多