如何在PHPUnit中管理互连测试

时间:2014-03-04 19:25:17

标签: php testing phpunit

如何减少类的测试中的冗余,其中几个方法只是同一类的其他一些方法的包装。
例如,如果我要测试一个类,该类根据其他方法验证的某些条件验证用户的帐户状态。这个类例如:

public function validateProfile(UserInterface $user)
{
    // check if profile is completed
}

public function validatePurchasedProducts(UserInterface $user)
{
    // check if user has purchased products
}

public function validateAssociatedCard(UserInterface $user)
{
    // check if user has a card associated with account
}

public function validateLoginStatus(UserInterface $user)
{
    return $this->validateProfile($user) 
        and $this->validatePurchasedProducts($user) 
        and $this->validateAssociatedCard($user);
}

我可以为前三种方法编写测试,但是当涉及到最后一种方法时,我必须重复我在最后3种方法中所做的完全相同的事情并将它们组合在一起。
它使测试过于冗余:

public function testUserHasValidProfileDetails()
{
    // arrange mocks // act // assert
}

public function testUserHasPurchasedProduct()
{
    // arrange mocks // act // assert
}

public function testUserHasCardAssociated()
{
    // arrange mocks // act // assert
}

public function testUserCanLogInToDashboard()
{
    // arrange mocks // act // assert - for profile validation
    // arrange mocks // act // assert - for products validation
    // arrange mocks // act // assert - for card validation
}

是否存在允许这种行为的方式(或PHPUnit中的功能)?我知道我可以使用@depends注释测试,但这不是那么重要。

3 个答案:

答案 0 :(得分:0)

我使用@depends将项目从一个测试传递到另一个测试,非常类似于示例中的数组。但是,在内部,我会根据您的尝试更改我的代码(然后是我的测试)。我通过让每个设置为有效的内部值来执行验证。这允许我测试每个函数并具有对象集的内部状态,以便我可以将其硬设置以供将来的测试。

private $ProfileValid;
private $PurchasedProducts;
private $CardAssociated;

public function validateProfile(UserInterface $user)
{
    // check if profile is completed
    $this->ProfileValid = true;
}

public function validatePurchasedProducts(UserInterface $user)
{
    // check if user has purchased products
    $this->PurchasedProducts = true;
}

public function validateAssociatedCard(UserInterface $user)
{
    // check if user has a card associated with account
    $this->CardAssociated = true;
}

public function validateLoginStatus(UserInterface $user)
{
    if(is_null( $this->ProfileValid) )
    {
        $this->validateProfile($user);
    }

    if(is_null( $this->PurchasedProducts) )
    {
        $this->validatePurchasedProducts($user) 
    }

    if(is_null( $this->CardAssociated) )
    {
        $this->validateAssociatedCard($user);
    }   
    return $this->ProfileValid && $this->PurchasedProducts && $this->CardAssociated;
}

然后,我可以创建一个对象并单独运行每个测试(使用或不使用模拟对象)并使用反射来查看内部变量是否设置正确。

最终测试然后创建对象并设置值(再次使用反射)并调用最终的validateLoginStatus()。由于我可以控制对象,我可以将一个或多个变量设置为null以调用测试。再次,如果需要的话,使用模拟。此外,Mock的设置可以是接受参数的测试代码中的内部函数。

这是一个类似的例子,用于测试我自己的内部迭代器,它来自一个抽象类。

class ABSTRACT_FOO extends FOO
{
    public function CreateFoo()  {   }
    public function CloseFoo()   {   }

    public function AddTestElement($String)
    {
        $this->Data[] = $String;
    }
}

class FOO_Test extends \PHPUnit_Framework_TestCase
{
    protected $FOOObject;

    protected function setUp()
    {
        $this->FOOObject = new ABSTRACT_FOO();
    }

    protected function tearDown()
    {
    }

    /**
     * Create the data array to have 3 items for test iteration
     */
    public function testCreateData()
    {
        $this->FOOObject->AddTestElement('Record 1');
        $this->FOOObject->AddTestElement('Record 2');
        $this->FOOObject->AddTestElement('Record 3');

        $ReflectionObject = new \ReflectionObject($this->FOOObject);

        $PrivateConnection = $ReflectionObject->getProperty('Data');
        $PrivateConnection->setAccessible(TRUE);
        $DataArray = $PrivateConnection->getValue($this->FOOObject);
        $this->assertEquals(3, sizeof($DataArray));

        return $this->FOOObject;        // Return Object for next test. Will have the 3 records
    }

    /**
     * @covers lib\FOO::rewind
     * @depends testCreateData
     */
    public function testRewind($DataArray)
    {
        $DataArray->Next();
        $this->assertGreaterThan(0, $DataArray->Key(), 'Ensure the iterator is not on the first record of the data.');
        $DataArray->Rewind();
        $this->assertEquals(0, $DataArray->Key());
    }

    /**
     * @covers lib\FOO::current
     * @depends testCreateData
     */
    public function testCurrent($DataArray)
    {
        $DataArray->Rewind();
        $Element = $DataArray->Current();
        $this->assertInternalType('string', $Element);
        $this->assertEquals('Record 1', $Element);
    }

    /**
     * @covers lib\FOO::key
     * @depends testCreateData
     */
    public function testKey($DataArray)
    {
        $DataArray->Rewind();
        $this->assertEquals(0, $DataArray->Key());
    }

    /**
     * @covers lib\FOO::next
     * @depends testCreateData
     */
    public function testNext($DataArray)
    {
        $DataArray->Rewind();
        $this->assertEquals(0, $DataArray->Key(), 'Ensure the iterator is at a known position to test Next() move on');

        $DataArray->Next();
        $this->assertEquals(1, $DataArray->Key());

        $Element = $DataArray->Current();
        $this->assertInternalType('string', $Element);
        $this->assertEquals('Record 2', $Element);
    }

    /**
     * @covers lib\FOO::valid
     * @depends testCreateData
     */
    public function testValid($DataArray)
    {
        $DataArray->Rewind();
        for($i = 0; $i < 3; ++ $i)      // Move through all 3 entries which are valid
        {
            $this->assertTrue($DataArray->Valid(), 'Testing iteration ' . $i);
            $DataArray->Next();
        }
        $this->assertFalse($DataArray->Valid());
    }
}

这使我可以在不重复加载数据的许多功能的情况下检查类。使用您首先使用的各个测试,还可以检查每个功能是否正常工作。如果您构建测试以对validateLoginStatus()进行测试,那么可以使用模拟来完成,只需要设置值,以确保所有组合都能正常工作,如果未来所有3都存在,则可能需要继续。我甚至会使用dataProvider功能来测试所有3个选项。

答案 1 :(得分:0)

validateLoginStatus 方法的目的是调用其他方法。因此,为了测试该方法,您不需要测试其他方法是否按预期工作(您在每个方法测试中都这样做)。您只需要确保调用其他方法,可能是正确的顺序。为此,您可以使用类的部分模拟,并模拟被调用的方法。

$object = $this->getMock(
    'Class', 
     array(
         'validateProfile', 
         'validatePurchasedProducts', 
         'validateLoginStatus'
     )
);

// configure method call expectations...

$object->validateLoginStatus($user);

这样做的另一个原因是,当某些方法按预期停止工作时,只有一个测试会失败。

答案 2 :(得分:0)

你应该最终重复自己。由于所有方法都属于同一个类,因此您不必知道正在调用其他方法。可以编写该类以将3个验证方法中的逻辑复制到最后一个方法中并且测试应该通过(不是这是一件好事)。但这可能是原始案例,并且重构以暴露类中的部分验证不应导致任何测试失败。

一般来说,如果某些东西难以测试,那么代码味道应该重新考虑您的设计。在你的情况下,我会将部分验证分解为自己的类,这些类将被传入并且这些可以被模拟。

IMO,嘲笑正在测试的系统是一种不好的做法,因为您现在正在指定系统的实现细节。这将使以后重构课程变得更加困难。