我知道这听起来很奇怪,为什么我应该测试如果我的测试通过了?一个简单的答案是,像您的其他代码一样,您的测试可能会有错误。同样,实现代码部分的逻辑可以引入您的测试不是为之设计的意外行为。让我们设置场景:
- 您的化妆应用程序是一个待办事项列表,允许与多个用户一起使用家庭帐户。
- 您有改进以允许二级帐户所有者。
- 您的主要和次要所有者的联系信息通过其用户设置进行了更新,但也更新了主帐户设置中的数据
- 始终需要昨天完成功能,而您的积压正在呼吸您的脖子。
- 您的测试主要是实现测试,专注于验证单个方法并假设其中一些数据状态。
一些模拟代码设置舞台
我将在PHP中写下这些示例,因为这是我的日常驱动程序,但是在任何代码库中和任何项目中都可以发生。您的代码开始看起来像这样。
class User_Account_Logic extends Logic_Base {
public function updateUserAccountInformation(array $user_information): array {
if ($this->userDataIsNotValid($user_information)) {
return $this->errors;
}
$this->User_Model->save($user_information);
if ($this->userShouldUpdateMainAccount($user_information)) {
$this->Main_Account_Logic->updateContactInformation($user_information);
}
return ['success' => true, 'message' => 'Successfully updated user information!'];
}
public function userShouldUpdateMainAccount(array $user): bool {
if ($user['account_type'] === $User_Model::OWNER) {
return true;
}
return false;
}
}
class Main_Account_Logic extends Logic_Base {
public function updateContactInformation(array $user_information): bool {
if ($user_information['account_type'] === User_Model::OWNER) {
return $this->updateMainAccountContact($user_information);
}
return false;
}
}
//Somewhere in your unit tests....
class User_Account_LogicTest extends TestCase {
public function getUserAccountMock() {
$Mock_Main_Account = $this->getMockBuilder(Main_Account_Logic)
->disableOriginalConstructor()
->onlyMethods(['updateContactInformation'])
->getMock();
$Mock_Main_Account->method('updateContactInformation')->willReturn(true);
$Mock_User_Logic = new class extends User_Account_Logic;
$Mock_User_Logic->Main_Account_Logic = $Mock_Main_Account;
return $Mock_User_Logic;
}
public function testThatFamilyMembersCanUpdateTheirSettings() {
$User_Account_Logic = $this->getUserAccountMock():
$Mock_User_Model = $this->getMockBuilder(User_Model::class)
->disableOriginalConstructor()
->onlyMethods(['save'])
->getMock();
$Mock_User_Model->expects($this->once())->method('save')->willReturn(true);
$User_Account_Logic->User_Model = $Mock_User_Model;
$result = $User_Account_Logic->updateUserAccountInformation([
'name' => 'foo bar',
'account_type' => User_Model::MEMBER.
]);
$this->assertTrue($result['success']);
}
}
没什么太疯狂了,您正在更新用户,如果他们是主要的,您也将更新主帐户信息。您拥有带有方便的辅助功能的单元测试,可以构建模拟用户帐户类,并且一切正常。您不介意,助手们假设了主帐户代码的返回,因为您正在实施测试,所以可能出了什么问题?
出了什么问题
您急于拿出此功能,并进行了一些更改,允许次要所有者更新二级所有者的主要帐户信息。该所有者可以做所有主要帐户持有人可以做的一切,这是一项要求的重要功能。因此,您所做的更改之一就是以下内容。
class User_Account_Logic extends Logic_Base {
public function updateUserAccountInformation(array $user_information): array {
if ($this->userDataIsNotValid($user_information)) {
return $this->errors;
}
$this->User_Model->save($user_information);
if ($this->userShouldUpdateMainAccount($user_information)) {
$this->Main_Account_Logic->updateContactInformation($user_information);
}
return ['success' => true, 'message' => 'Successfully updated user information!'];
}
public function userShouldUpdateMainAccount(array $user): bool {
if ($user['account_type'] >= $User_Model::SECONDARY) {
return true;
}
return false;
}
}
class Main_Account_Logic extends Logic_Base {
public function updateContactInformation(array $user_information): bool {
if ($user_information['account_type'] === User_Model::OWNER) {
return $this->updateMainAccountContact($user_information);
}
return $this->updateSecondaryAccountInformation($user_information);
}
}
在这些文件中,您要做的就是快速更改User_Account_Logic
方法userShouldUpdateMainAccount
中的===
,并添加了一个调用来更新Main_Account_Logic
类中的辅助帐户联系信息。您刚刚做的是使除所有者以外的二级帐户所有者以外的任何帐户用户。 >
而不是<
的快速错字仅使任何用户能够将辅助用户更新为其帐户。
测试会抓住它...对吗?
否,不会因为您不更新测试,因为它们仍然通过,您只是更改了哪些帐户击中更新主帐户方法,因此任何内容都不应失败。由于您专注于实施测试而不是行为测试,因此您嘲笑Main_Account_Logic
和Main_Account_LogicTest
的响应假设给出了数据的状态,以使您的所有测试通过。足够好,推动那个坏男孩活着。
测试如何停止这种情况?
您可以添加一件事,以防止该测试在不应该的情况下通过。那一件事是测试不应该发生的事情。
class User_Account_LogicTest extends TestCase {
public function getUserAccountMock(int $main_account_hits) {
$Mock_Main_Account = $this->getMockBuilder(Main_Account_Logic)
->disableOriginalConstructor()
->onlyMethods(['updateContactInformation'])
->getMock();
$Mock_Main_Account->expects($this->exactly($main_account_hits))
->method('updateContactInformation')
->willReturn(true);
$Mock_User_Logic = new class extends User_Account_Logic;
$Mock_User_Logic->Main_Account_Logic = $Mock_Main_Account;
return $Mock_User_Logic;
}
public function testThatFamilyMembersCanUpdateTheirSettings() {
$User_Account_Logic = $this->getUserAccountMock(0):
$Mock_User_Model = $this->getMockBuilder(User_Model::class)
->disableOriginalConstructor()
->onlyMethods(['save'])
->getMock();
$Mock_User_Model->expects($this->once())->method('save')->willReturn(true);
$User_Account_Logic->User_Model = $Mock_User_Model;
$result = $User_Account_Logic->updateUserAccountInformation([
'name' => 'foo bar',
'account_type' => User_Model::MEMBER.
]);
$this->assertTrue($result['success']);
}
}
补充说,$this->exactly(0)
会导致该测试失败。 phpunit会报告一个错误,因为您期望该方法被称为0次,并且被称为一次。然后,您会遇到自己的错误,并且您将阻止用户的儿子拥有可怕的密码,并使他的帐户受到妥协,从而使攻击者所有者级别访问用户的帐户。
结论
确保您还测试不应该发生的事情可以有助于提高测试的可靠性并提前捕获错误。测试不仅是关于应该发生的事情,而且是您的系统不应该发生的事情并忽略不应该发生的事情可能最终会以与您假设完全相反的代码。