如何在PHPUnit中管理互连测试

如何在PHPUnit中管理互连测试,php,testing,phpunit,Php,Testing,Phpunit,在一个类的测试中,如果两个方法只是同一个类的几个其他方法的包装器,您将如何减少冗余。 例如,如果我要测试一个类,该类根据其他方法验证的某些条件验证用户的帐户状态。例如,本课程: public function validateProfile(UserInterface $user) { // check if profile is completed } public function validatePurchasedProducts(UserInterface $user) {

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

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种方法编写测试,但当涉及到最后一种方法时,我必须重复我在最后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对测试进行注释,但这并不是问题所在。

我使用@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,以调用测试。同样,如果需要,可以使用模拟。此外,模拟的设置可以是测试代码中接受参数的内部函数

下面是一个类似的示例,用于从抽象类测试我自己的内部迭代器

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());
    }
}
这使我能够检查类,而无需在加载数据时重复大量功能。首先,使用单独的测试还可以检查每个功能是否正常工作。如果您将测试结构化为对ValidateLogistatus进行测试,那么可以使用一个模拟来完成,该模拟只包含您希望设置的值,以确保所有组合都能正常工作,如果将来没有全部3个组合,您可能希望继续工作。我甚至会使用dataProvider功能测试所有3个选项。

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

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

// configure method call expectations...

$object->validateLoginStatus($user);

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

您应该重复自己的测试。由于所有方法都是同一个类的一部分,所以您不一定知道正在调用其他方法。该类可以编写为将3个验证方法中的逻辑复制到最后一个方法中,并且测试应该通过,而不是说这是一件好事。但这可能是原始情况,重构以公开类中的部分验证不应导致任何测试失败

一般来说,如果某个东西很难测试,那么您应该重新考虑您的设计。在您的情况下,我会将部分验证分解为它们自己的类,这些类将被传入,而这些类可能会被模拟


在我看来,模拟正在测试的系统是一种糟糕的做法,因为您现在正在指定系统的实现细节。这将使以后重构类变得更加困难。

甜蜜而简单!这正是我喜欢的方式。这真的不是一个好的测试实践。您正在指定被测试类的实现细节。