How to test factory classes?

2020-07-17 05:57发布

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.

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?

3条回答
太酷不给撩
2楼-- · 2020-07-17 06:18

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(). 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.

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. 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. Your factory is intended to created MyClass objects so if that class is undefined then your test should definitely fail.

Having failing tests while your developing is also not a bad thing.

查看更多
兄弟一词,经得起流年.
3楼-- · 2020-07-17 06:26

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


For tests, you can just create your MockBuilder which extends from your Builder:

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. The creating is a detail of the factory, nothing you externalize. So it should be encapsulated. Hence the MockBuilder for tests. You switch the Factory.

查看更多
趁早两清
4楼-- · 2020-07-17 06:36

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).

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.

... 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(); 
    }
}
查看更多
登录 后发表回答