PHPUnit 数据提供者

测试方法通常需要测试数据。要完全测试某些方法,你需要为每个可能的测试条件提供不同的数据集。当然,你可以使用循环手动执行此操作,如下所示:

...
public function testSomething()
{
    $data = [...];
    foreach($data as $dataSet) {
       $this->assertSomething($dataSet);
    }
}
... 

有人可以发现它很方便。但是这种方法存在一些缺点。首先,如果测试函数接受多个参数,则必须执行其他操作来提取数据。其次,在失败时,如果没有附加消息和调试,就很难区分出故障数据集。第三,PHPUnit 提供了使用数据提供程序处理测试数据集的自动方法。

数据提供程序是一个函数,应返回特定测试用例的数据。

数据提供程序方法必须是公共的,并返回一个数组数组或一个实现 Iterator 接口的对象,并为每个迭代步骤生成一个数组。对于作为集合一部分的每个数组,将调用测试方法,并将数组的内容作为其参数。

要在测试中使用数据提供程序,请使用 @dataProvider annotation 以及指定的数据提供程序函数的名称:

/**
* @dataProvider dataProviderForTest
*/
public function testEquals($a, $b)
{
    $this->assertEquals($a, $b);
}

public function dataProviderForTest()
{
    return [
        [1,1],
        [2,2],
        [3,2] //this will fail
    ];
}

数组数组

请注意,dataProviderForTest() 返回数组数组。每个嵌套数组都有两个元素,它们将逐一填充 testEquals() 的必要参数。如果没有足够的元素,这样的错误将被抛出 Missing argument 2 for Test::testEquals()。PHPUnit 将自动循环数据并运行测试:

public function dataProviderForTest()
{
    return [
        [1,1], // [0] testEquals($a = 1, $b = 1)
        [2,2], // [1] testEquals($a = 2, $b = 2)
        [3,2]  // [2] There was 1 failure: 1) Test::testEquals with data set #2 (3, 4)
    ];
}

为方便起见,可以命名每个数据集。检测失败的数据会更容易:

public function dataProviderForTest()
{
    return [
        'Test 1' => [1,1], // [0] testEquals($a = 1, $b = 1)
        'Test 2' => [2,2], // [1] testEquals($a = 2, $b = 2)
        'Test 3' => [3,2]  // [2] There was 1 failure: 
                           //     1) Test::testEquals with data set "Test 3" (3, 4)
    ];
}

迭代器

class MyIterator implements Iterator {
    protected $array = [];

    public function __construct($array) {
        $this->array = $array;
    }

    function rewind() {
        return reset($this->array);
    }

    function current() {
        return current($this->array);
    }

    function key() {
        return key($this->array);
    }

    function next() {
        return next($this->array);
    }

    function valid() {
        return key($this->array) !== null;
    }
}
...

class Test extends TestCase
{
    /**
     * @dataProvider dataProviderForTest
     */
    public function testEquals($a)
    {
        $toCompare = 0;

        $this->assertEquals($a, $toCompare);
    }

    public function dataProviderForTest()
    {
        return new MyIterator([
            'Test 1' => [0],
            'Test 2' => [false],
            'Test 3' => [null]
        ]);
    }
}

如你所见,简单的迭代器也可以工作。

请注意,即使对于单个参数,数据提供者也必须返回数组 [$parameter]

因为如果我们将 current() 方法(实际上每次迭代返回数据)更改为:

function current() {
    return current($this->array)[0];
}

或者更改实际数据:

return new MyIterator([
            'Test 1' => 0,
            'Test 2' => false,
            'Test 3' => null
        ]);

我们会收到一个错误:

There was 1 warning:

1) Warning
The data provider specified for Test::testEquals is invalid.

当然,在简单数组上使用 Iterator 对象是没有用的。它应该为你的案例实现一些特定的逻辑。

发电机

它没有明确说明并在手册中显示,但你也可以使用生成器作为数据提供者。请注意,Generator 类实际上实现了 Iterator 接口。

这里有一个使用 DirectoryIteratorgenerator 结合的例子:

/**
 * @param string $file
 *
 * @dataProvider fileDataProvider
 */
public function testSomethingWithFiles($fileName)
{
    //$fileName is available here
    
    //do test here
}

public function fileDataProvider()
{
    $directory = new DirectoryIterator('path-to-the-directory');

    foreach ($directory as $file) {
        if ($file->isFile() && $file->isReadable()) {
            yield [$file->getPathname()]; // invoke generator here.
        }
    }
}

注意提供者 yields 是一个数组。你将收到无效的数据提供者警告。