有没有办法拥有动态dataProvider?

时间:2018-04-24 04:51:07

标签: php unit-testing phpunit

我正在为一系列方法编写单元测试,每个方法都解析一个xml字符串并返回一个对象 1 。我目前正在使用Data Providers来测试不同版本的xml字符串。以下是我正在做的一个例子:

class TestSubjectTest extends PHPUnit_Framework_TestCase {
    public function employeeXmlProvider() {
        $array = [
            [
                '<?xml version="1.0" encoding="UTF-8"?><root><employee><status>1</status><name>John</name></employee></root>',
                true
            ],
            [
                '<?xml version="1.0" encoding="UTF-8"?><root><employee><status>0</status><name>Sally</name></employee></root>',
                false
            ]
        ];
    }

    public function customerXmlProvider() {
        $array = [
            [
                '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>',
                true
            ],
            [
                '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>0</status><name>Sally</name></customer></root>',
                false
            ]
        ];
    }

    /**
     * @dataProvider employeeXmlProvider
     */
    public function testMethodThatUsesEmployee($xml, $expectedStatus) {
        $testSubject = new TestSubject();
        $result = $testSubject->parseEmployeeAndReturnObject($xml);
        $this->assertInstanceOf(Person:class, $result);
        $this->assertEquals($expectedStatus, $result->active);
    }

    /**
     * @dataProvider customerXmlProvider
     */
    public function testMethodThatUsesCustomer($xml, $expectedStatus) {
        $testSubject = new TestSubject();
        $result = $testSubject->parseCustomerAndReturnObject($xml);
        $this->assertInstanceOf(Person:class, $result);
        $this->assertEquals($expectedStatus, $result->active);
    }
}

每种方法使用的XML字符串仅在其中一个顶级标记的名称中有所不同。在上面的示例代码中,两个提供程序之间的唯一区别是,一个使用employee标记,另一个使用customer标记。有很多方法可以像这样测试。我想知道是否可以创建一个数据提供程序,它可以有条件地更改XML字符串中的标记,具体取决于它用于的测试函数。

[1]:在我的实际代码中,每个方法实际上都调用了一个用XML响应的第三方API。 API调用由传递到被测试类的构造函数的服务执行。我正在使用Mock对象将数据调用返回值替换为数据提供程序中的XML字符串。这个细节并不重要,所以在上面的示例代码中,我只是将XML直接传递给方法。

1 个答案:

答案 0 :(得分:1)

首先让我们看看这些

的内容是什么
/**
 * @dataProvider employeeXmlProvider
 */
public function testMethodThatUsesEmployee($xml, $expectedStatus) {
    $testSubject = new TestSubject();
    $result = $testSubject->parseEmployeeAndReturnObject($xml);
    $this->assertInstanceOf(Person:class, $result);
    $this->assertEquals($expectedStatus, $result->active);
}

/**
 * @dataProvider customerXmlProvider
 */
public function testMethodThatUsesCustomer($xml, $expectedStatus) {
    $testSubject = new TestSubject();
    $result = $testSubject->parseCustomerAndReturnObject($xml);
    $this->assertInstanceOf(Person:class, $result);
    $this->assertEquals($expectedStatus, $result->active);
}

这就是我所看到的

/**
 * @dataProvider genericXmlProvider
 */
public function testGenericMethod($parsemethod, $xml, $resultclass, $expectedStatus) {
    $testSubject = new TestSubject();
    //you could throw an if(!method_exists($testSubject, $parsemethod)) throw new .... in here if you want.
    $result = $testSubject->{$parsemethod}($xml);
    $this->assertInstanceOf($resultclass, $result);
    $this->assertEquals($expectedStatus, $result->active);
}

因此,在基本形式中,您需要提供者传递这些额外的两件事。

public function genericXmlProvider(){
  $array = [
        [
            'parseEmployeeAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>',
             Person:class,
             true
        ],
        [
            'parseEmployeeAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>0</status><name>Sally</name></customer></root>',
             Person:class,
            false
        ],
        [
            'parseCustomerAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>',
             Person:class
            true
        ],
        [               
            'parseCustomerAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>0</status><name>Sally</name></customer></root>',
             Person:class
            false
        ]
    ];
}

所以现在我们已经有效地将这些合并在一起。我们可以就此达成一致。如果将parse函数作为字符串传入,结果类也作为字符串传递,那么我们可以统一这些方法。正确的吗?

然而,正如你所看到的,这将很快变得“失控”并成为一个真正的噩梦来维持。因此,我不会编辑维护这个庞大的“Stuff”数组,而是为“test”文件创建一个文件夹。我将在这个文件夹中将其称为“providerTests”,我们将把这些文件放入其中作为他们自己的迷你PHP文件。

以下是我称之为emptest1.php的第一个数组的示例。

<?php
    return 
       [
            'parseEmployeeAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>',
             Person:class,
             true
        ];

现在这是在提供程序而不是这个大的沉没数组中做的,我们可以从providerTests提取数据并构造我们的数组。

public function genericXmlProvider(){
     $providers = array_diff(scandir(__DIR__.'/providerTests'), ['.', '..']);
     $data = [];
     foreach($providers as $provider){

         $item = require $provider;
         //this is optional, I thought I would toss in a bit of validation on the input files. You could make them classes and have an interface etc. but that might be overkill.
         if(!is_array($item)) throw new Exception("Provider data error in file $provider");
         $data[] = $item;

     }
     return $data;
}

所以现在我们可以做的就是删除带有这些数组提供程序的新文件或任何你想要调用它们的文件,它们会被提供程序吸入并返回给测试函数。这应该使测试方式更加愉快。

我没有测试任何这个,但它可能会起作用....大声笑

<强>更新

最后一个想法是我会在方法的输入和文件的数组中包含文件的名称。然后在'assert'消息中,我认为这是你可以将信息放入的第三个参数。然后当测试失败时,我们将有办法回溯到它来自哪个文件。

这样的事情

emptest1.php

<?php
    return 
       [
            'parseEmployeeAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>',
             Person:class,
             true,
             __FILE__ //we will use the magic __FILE__ constant for back tracking
        ];

然后

/**
 * @dataProvider genericXmlProvider
 */
public function testGenericMethod($parsemethod, $xml, $resultclass, $expectedStatus, $providerSource) {
    $testSubject = new TestSubject();
    $result = $testSubject->{$parsemethod}($xml);
    $this->assertInstanceOf($resultclass, $result, "Provided by $providerSource");
    $this->assertEquals($expectedStatus, $result->active, "Provided by $providerSource");
}

然后当测试失败时,它会说:

"Provided by {path}/providerTests/emptest1.php"

我们可以轻松地将其回溯到注入的提供者存根。

是的,我很聪明,我知道......

<强> UPDATE1

重新

  

实际上我希望我的dataProvider有一种方法可以返回类似

的内容

让我们尝试最小化这一点,添加一些复杂性,但这是一次复杂度,而不是在我们的“配置”文件中有很多输入的所有时间复杂性。

对于Person:class,可以合理地假设对象类型不会从一个“类型”变为我们需要做的测试的另一个“类型”(我们的意思是你)。例如,类型类似于“员工”或“客户”。

我们可以使用文件名的一些创意命名约定来消除parsemethodstatus。我们将使用这样的文件名架构:

{parseMethod}_{status}_{instance}

或者我们可以将方法缩短为

parseEmployee_0_1.xml
parseEmployee_1_1.xml

parseCustomer_0_1.xml
parseCustomer_1_1.xml

我们在编译提供程序时实际可以得到的文件名__FILE__,就像我们从scandir那样。不知道为什么我没有想到这一点。

这样可以减少我们的配置:

<?php
    return 
       [
            'parseEmployeeAndReturnObject',
            '<?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>',
             Person:class,
             true
        ];

就这样:

  <?xml version="1.0" encoding="UTF-8"?><root><customer><status>1</status><name>John</name></customer></root>

您可能已经注意到我已将扩展程序从.php更改为.xml。这是因为我们不再需要它成为PHP。现在留在文件正文中的所有内容都是实际的XML,当然这需要对我们的提供者进行一些极端的修改:

public function genericXmlProvider(){
     //these are common per type of object we are parsing, which is reasonable
     $default = [
         'employee' =>[
              'parseMethod'=>'parseEmployeeAndReturnObject',
              'resultClass' => Person::class
         ],
         'customer' =>[
              'parseMethod'=>'parseCustomerAndReturnObject',
              'resultClass' => Person::class
         ]               
     ];
     //dynamically compile the pattern based on the default array's keys
     $pattern = '/\/parse(?P<method>'.implode('|', array_keys($default)).')_(?P<status>[0-9])_[0-9]\.xml$/i';
     //should be this  \/parse(?P<method>employee|customer)_(?P<status>[0-9])_[0-9]\.xml$/i

     //scan for files and remove '.' and '..', which is typical of scandir
     $providers = array_diff(scandir(__DIR__.'/providerTests'), ['.', '..']);
     $data = [];
     foreach($providers as $providerSource){
         //parse the filename
        if(preg_match($pattern, $providerSource, $match){ 
             $xml = trim(file_get_contents($providerSource)); //trim just because we can
             //I put the argument names in so it's a bit easier to read
             $data[] = [
                 $default[$match['method']]['parseMethod'], //$parsemethod,
                 $xml,
                 $default[$match['method']]['resultClass'], //$resultclass,
                 $match['status'], // $expectedStatus,
                 $providerSource
             ];
        }else{
            //[optional] throw error for no filename match
        }

     }
     return $data;
}

这应该最小化每个输入文件中所需的“Stuff”数量,这很好。只要我们坚持文件名的命名约定,这就可以工作。这有一些优点和缺点。它使提供程序更复杂,并对文件名添加命名约定限制,这本身并不一定是坏事,因为它可以帮助组织文件并保持名称合理。

一个小缺点是,如果我们添加一个类型,我们可能必须更改一些代码,之前它只包含在文件正文中。但是,与仅使用大型数组相比,我们需要添加的代码量是最小的,并且从长远来看,它更多是D.R.Y。

幸运的是,我们根本不需要重构测试方法,因为我们保持相同的输入,我们只是改变了存储它们的方式。

如果需要,请参阅我们可以最小化每个文件的个别要求。我们可以用PHP做任何我们想象的事情。实际上,这很酷。作为奖励,你必须亲眼目睹我从无到有的过程中走向伟大的事情......哈哈。