[英]How to manage interconnected tests in 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);
}
我可以為前三種方法編寫測試,但是當涉及到最后一種方法時,我必須重復與在后三種方法中所做的完全相同的操作並將其組合在一起。
它會使測試變得多余:
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());
}
}
這使我可以檢查類,而無需在加載數據時重復很多功能。 首先,您可以使用單個測試來檢查每個功能是否正常運行。 如果您對測試進行結構化以對validateLoginStatus()進行測試,則可以使用僅設置您要設置的值的模擬來完成,以確保所有組合都能正常工作,如果將來不存在全部3個,則可以繼續。 我什至會使用所有3個選項的測試來使用dataProvider功能進行嘗試。
validateLoginStatus方法的目的是調用其他方法。 因此,對於測試該方法,您無需測試其他方法是否按預期工作(在每個方法測試中都可以做到)。 您只需要確保以正確的順序調用其他方法即可。 為此,您可以使用該類的部分模擬,並模擬所調用的方法。
$object = $this->getMock(
'Class',
array(
'validateProfile',
'validatePurchasedProducts',
'validateLoginStatus'
)
);
// configure method call expectations...
$object->validateLoginStatus($user);
這樣做的另一個原因是,當某些方法按預期停止工作時,只有一個測試會失敗。
您應該結束自己的重復。 由於所有方法都屬於同一類,因此您不必知道其他方法都在被調用。 可以編寫該類以將3個驗證方法中的邏輯復制到最后一個方法中,並且測試應該通過(這不是一件好事)。 但這可能是原始情況,並且進行重構以暴露類中的部分驗證不應導致任何測試失敗。
一般來說,如果難以測試,則應重新考慮設計的代碼味道。 在您的情況下,我會將部分驗證分為自己的類,這些類將被傳入,並且可以被嘲笑。
IMO,模擬被測系統是一種不好的做法,因為您現在要指定系統的實現細節。 這會使以后重構類變得更加困難。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.