如何减少类的测试中的冗余,其中几个方法只是同一类的其他一些方法的包装。
例如,如果我要测试一个类,该类根据其他方法验证的某些条件验证用户的帐户状态。这个类例如:
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
注释测试,但这不是那么重要。
答案 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,嘲笑正在测试的系统是一种不好的做法,因为您现在正在指定系统的实现细节。这将使以后重构课程变得更加困难。