如何通过 Symfony 中的 PHPunit 模拟函数以测试持久化到数据库中
How to mock a function to test persist into database by PHPunit in Symfony
这是第一次使用 PHPunit 创建单元测试来测试我的 Web 应用程序的业务模型。我很困惑地嘲笑我的 class 用于持久化实体。这里是newUser()的函数。
class AccountBM extends AbstractBusinessModel {
public function newUser(Account $account) {
$salt = StringHelper::generateRandomString ( "10" );
$account->setUsername ( $account->getEmail () );
$invitation_code = hash ( 'md5', $account->getEmail () );
$account->setInviationCode ( $invitation_code );
$account->setPassword ( $this->encoder->encodePassword ( $account->getPassword (), $salt ) );
$account->setSalt ( $salt );
$this->persistEntity ( $account );
return $account;
}
}
然后,我尝试为此函数 testNewUser() 创建一个测试单元
public function testNewUser() {
$account = $this->newAccountEntity ();
$account_bm =$this->getMockBuilder('AccountBM')
->setMethods(array('newUser'))
->disableOriginalConstructor()
->getMock();
$account=$account_bm->newUser($account);
// compare setter value with saved entity
$this->assertEquals ( 'test@test.com', $account->getEmail () );
$this->assertEquals ( '123456789', $account->getPhone () );
$this->assertEquals ( '987654321', $account->getMobilePhone () );
$this->assertEquals ( 'Mr', $account->getTitle () );
$this->assertEquals ( 'test', $account->getFirstName () );
$this->assertEquals ( 'test', $account->getLastName () );
$this->assertEquals ( 'male', $account->getGender () );
$this->assertEquals ( '1', $account->getCompanyLogo () );
$this->assertEquals ( AccountType::IDS, $account->getUserType () );
$this->assertEquals ( RegionType::NCN, $account->getRegion () );
$this->assertEquals ( hash ( 'md5', $account->getEmail () ), $account->getInviationCode () );
$this->assertEquals ( hash ( 'sha256', $account->getSalt () . "test" ), $account->getPassword () );
}
public function newAccountEntity() {
$account = new Account ();
// set account
$account->setEmail ( "test@test.com" );
$account->setPassword ( "test" );
$account->setPhone ( "123456789" );
$account->setMobilePhone ( "987654321" );
$account->setTitle ( "Mr" );
$account->setFirstName ( "test" );
$account->setLastName ( "test" );
$account->setGender ( "male" );
$account->setCompanyLogo ( "1" );
$account->setUserType ( AccountType::IDS );
$account->setRegion ( RegionType::NCN );
return $account;
}
但它似乎不会模拟方法 "newUser" 并且它不适用于我的实体。测试代码有什么问题吗?谢谢
回答你的问题:
模拟方法不是您要测试的方法,而是您不想测试的方法。在你的代码中,你正在创建一个你想要测试的 class 的模拟对象(这完全没问题,尽管你通常模拟你不想测试的依赖 classes )并告诉模拟对象, newUser()
是您要模拟的方法。
相反,您想测试 newUser()
并且可能想要测试 persistEntity()
:
$account_bm =$this->getMockBuilder('AccountBM')
->setMethods(array('persistEntity'))
->disableOriginalConstructor()
->getMock();
$account=$account_bm->newUser($account);
您可能还想查看有关模拟对象的 PHPUnits 文档:
https://phpunit.de/manual/current/en/test-doubles.html
二、PHPUnit开发的几个小技巧:
在你的测试中你有很多断言。最好的情况是每个测试只有一个断言,而 PHPUnit 实际上可以很好地帮助您,因为它几乎可以比较所有内容。因此,无需断言对象的每个值,您只需断言一个对象与另一个对象,如果这些对象不相等,PHPUnit 将向您显示差异。
此外,您甚至可能希望在您的实体中遗漏一些您不需要的信息。您的数据库可能需要名字和姓氏,但您的方法不需要,因此您的测试也不需要。您不必编写那么多代码,而且可读性更高。
但这还不是全部,断言也可以使用模拟方法来完成。
像这样尝试你的测试:
public function testNewUser() {
// this is the entity for the method
$account = new Account();
$account->setEmail ( "test@test.com" );
$account->setPassword ( "test" );
// this is how the entity should look like when it's done
$expectedAccount = new Account ();
$expectedAccount ->setEmail ( "test@test.com" );
$expectedAccount ->setInviationCode ( md5("test@test.com") );
// changes needed, how does your encoder create the password hash?
// Or even better: Also mock your encoder, you don't want to test it here
$expectedAccount ->setPassword ( "test" );
// As this is a random value, you might want to mock it as well
// mock a static call
$expectedAccount ->setSalt ( "10" );
/** @var AccountBM|\PHPUnit_Framework_MockObject_MockObject $account_bm */
$account_bm = $this->getMockBuilder('AccountBM')
->setMethods(array('persistEntity'))
->disableOriginalConstructor()
->getMockForAbstractClass();
$account_bm->expects($this->once())
->method("persistEntity")
->with($expectedAccount);
$account_bm->newUser($account);
}
您现在可以使用 $expectedEntity
断言返回的实体,但我将其省略是为了向您展示,期望 一个方法被调用 使用 特定参数将已经触发断言。
如您所见,上述测试将失败并向您显示哪些属性是错误的。为什么会这样?
好吧,有两个 ToDo
标记:您的两个值是由 class 之外的方法生成的,应该被嘲笑。那些是 $this->encoder->encodePassword()
和 StringHelper::generateRandomString()
。
因此,为 encoder
创建一个模拟对象并模拟一个静态方法,您将它放在 AccountBM
中的单独方法中以包装它:
帐号Bm
public function newUser(Account $account) {
$salt = $this->generateSalt("10");
$account->setUsername ( $account->getEmail () );
$invitation_code = hash ( 'md5', $account->getEmail () );
$account->setInviationCode ( $invitation_code );
$account->setPassword ( $this->encoder->encodePassword ( $account->getPassword (), $salt ) );
$account->setSalt ( $salt );
$this->persistEntity ( $account );
return $account;
}
public function generateSalt($string)
{
return StringHelper::generateRandomString ( $string );
}
现在将此方法与 encoder
:
一起模拟
public function testNewUser() {
$account = new Account();
$account->setEmail ( "test@test.com" );
$account->setPassword ( "test" );
$expectedAccount = new Account ();
$expectedAccount ->setEmail ( "test@test.com" );
$expectedAccount ->setInviationCode ( md5("test@test.com") );
$expectedAccount ->setPassword ( "test" );
$expectedAccount ->setSalt ( "10" );
/** @var AccountBM|\PHPUnit_Framework_MockObject_MockObject $account_bm */
$account_bm = $this->getMockBuilder('AccountBM')
->setMethods(array('persistEntity', 'generateSalt'))
->disableOriginalConstructor()
->getMockForAbstractClass();
// mock the wrapper for the static method
$account_bm->expects($this->once())
->method("generateSalt")
->with("10")
->will($this->returnValue("10"));
// mock the encoder and set it to your object
$encoder = $this->getMockBuilder("stdClass")
->setMethods(array("encodePassword"))
->disableOriginalConstructor()
->getMock();
$encoder->expects($this->once())
->method("encodePassword")
->with("test", "10")
->will($this->returnValue(md5("test10")));
$account_bm->setEncoder($encoder);
$account_bm->expects($this->once())
->method("persistEntity")
->with($expectedAccount);
$account_bm->newUser($account);
}
但测试仍然失败 - 我会留给你替换错误的值,以检查你是否理解代码的工作原理;-)
这是第一次使用 PHPunit 创建单元测试来测试我的 Web 应用程序的业务模型。我很困惑地嘲笑我的 class 用于持久化实体。这里是newUser()的函数。
class AccountBM extends AbstractBusinessModel {
public function newUser(Account $account) {
$salt = StringHelper::generateRandomString ( "10" );
$account->setUsername ( $account->getEmail () );
$invitation_code = hash ( 'md5', $account->getEmail () );
$account->setInviationCode ( $invitation_code );
$account->setPassword ( $this->encoder->encodePassword ( $account->getPassword (), $salt ) );
$account->setSalt ( $salt );
$this->persistEntity ( $account );
return $account;
}
}
然后,我尝试为此函数 testNewUser() 创建一个测试单元
public function testNewUser() {
$account = $this->newAccountEntity ();
$account_bm =$this->getMockBuilder('AccountBM')
->setMethods(array('newUser'))
->disableOriginalConstructor()
->getMock();
$account=$account_bm->newUser($account);
// compare setter value with saved entity
$this->assertEquals ( 'test@test.com', $account->getEmail () );
$this->assertEquals ( '123456789', $account->getPhone () );
$this->assertEquals ( '987654321', $account->getMobilePhone () );
$this->assertEquals ( 'Mr', $account->getTitle () );
$this->assertEquals ( 'test', $account->getFirstName () );
$this->assertEquals ( 'test', $account->getLastName () );
$this->assertEquals ( 'male', $account->getGender () );
$this->assertEquals ( '1', $account->getCompanyLogo () );
$this->assertEquals ( AccountType::IDS, $account->getUserType () );
$this->assertEquals ( RegionType::NCN, $account->getRegion () );
$this->assertEquals ( hash ( 'md5', $account->getEmail () ), $account->getInviationCode () );
$this->assertEquals ( hash ( 'sha256', $account->getSalt () . "test" ), $account->getPassword () );
}
public function newAccountEntity() {
$account = new Account ();
// set account
$account->setEmail ( "test@test.com" );
$account->setPassword ( "test" );
$account->setPhone ( "123456789" );
$account->setMobilePhone ( "987654321" );
$account->setTitle ( "Mr" );
$account->setFirstName ( "test" );
$account->setLastName ( "test" );
$account->setGender ( "male" );
$account->setCompanyLogo ( "1" );
$account->setUserType ( AccountType::IDS );
$account->setRegion ( RegionType::NCN );
return $account;
}
但它似乎不会模拟方法 "newUser" 并且它不适用于我的实体。测试代码有什么问题吗?谢谢
回答你的问题:
模拟方法不是您要测试的方法,而是您不想测试的方法。在你的代码中,你正在创建一个你想要测试的 class 的模拟对象(这完全没问题,尽管你通常模拟你不想测试的依赖 classes )并告诉模拟对象, newUser()
是您要模拟的方法。
相反,您想测试 newUser()
并且可能想要测试 persistEntity()
:
$account_bm =$this->getMockBuilder('AccountBM')
->setMethods(array('persistEntity'))
->disableOriginalConstructor()
->getMock();
$account=$account_bm->newUser($account);
您可能还想查看有关模拟对象的 PHPUnits 文档: https://phpunit.de/manual/current/en/test-doubles.html
二、PHPUnit开发的几个小技巧:
在你的测试中你有很多断言。最好的情况是每个测试只有一个断言,而 PHPUnit 实际上可以很好地帮助您,因为它几乎可以比较所有内容。因此,无需断言对象的每个值,您只需断言一个对象与另一个对象,如果这些对象不相等,PHPUnit 将向您显示差异。
此外,您甚至可能希望在您的实体中遗漏一些您不需要的信息。您的数据库可能需要名字和姓氏,但您的方法不需要,因此您的测试也不需要。您不必编写那么多代码,而且可读性更高。
但这还不是全部,断言也可以使用模拟方法来完成。
像这样尝试你的测试:
public function testNewUser() {
// this is the entity for the method
$account = new Account();
$account->setEmail ( "test@test.com" );
$account->setPassword ( "test" );
// this is how the entity should look like when it's done
$expectedAccount = new Account ();
$expectedAccount ->setEmail ( "test@test.com" );
$expectedAccount ->setInviationCode ( md5("test@test.com") );
// changes needed, how does your encoder create the password hash?
// Or even better: Also mock your encoder, you don't want to test it here
$expectedAccount ->setPassword ( "test" );
// As this is a random value, you might want to mock it as well
// mock a static call
$expectedAccount ->setSalt ( "10" );
/** @var AccountBM|\PHPUnit_Framework_MockObject_MockObject $account_bm */
$account_bm = $this->getMockBuilder('AccountBM')
->setMethods(array('persistEntity'))
->disableOriginalConstructor()
->getMockForAbstractClass();
$account_bm->expects($this->once())
->method("persistEntity")
->with($expectedAccount);
$account_bm->newUser($account);
}
您现在可以使用 $expectedEntity
断言返回的实体,但我将其省略是为了向您展示,期望 一个方法被调用 使用 特定参数将已经触发断言。
如您所见,上述测试将失败并向您显示哪些属性是错误的。为什么会这样?
好吧,有两个 ToDo
标记:您的两个值是由 class 之外的方法生成的,应该被嘲笑。那些是 $this->encoder->encodePassword()
和 StringHelper::generateRandomString()
。
因此,为 encoder
创建一个模拟对象并模拟一个静态方法,您将它放在 AccountBM
中的单独方法中以包装它:
帐号Bm
public function newUser(Account $account) {
$salt = $this->generateSalt("10");
$account->setUsername ( $account->getEmail () );
$invitation_code = hash ( 'md5', $account->getEmail () );
$account->setInviationCode ( $invitation_code );
$account->setPassword ( $this->encoder->encodePassword ( $account->getPassword (), $salt ) );
$account->setSalt ( $salt );
$this->persistEntity ( $account );
return $account;
}
public function generateSalt($string)
{
return StringHelper::generateRandomString ( $string );
}
现在将此方法与 encoder
:
public function testNewUser() {
$account = new Account();
$account->setEmail ( "test@test.com" );
$account->setPassword ( "test" );
$expectedAccount = new Account ();
$expectedAccount ->setEmail ( "test@test.com" );
$expectedAccount ->setInviationCode ( md5("test@test.com") );
$expectedAccount ->setPassword ( "test" );
$expectedAccount ->setSalt ( "10" );
/** @var AccountBM|\PHPUnit_Framework_MockObject_MockObject $account_bm */
$account_bm = $this->getMockBuilder('AccountBM')
->setMethods(array('persistEntity', 'generateSalt'))
->disableOriginalConstructor()
->getMockForAbstractClass();
// mock the wrapper for the static method
$account_bm->expects($this->once())
->method("generateSalt")
->with("10")
->will($this->returnValue("10"));
// mock the encoder and set it to your object
$encoder = $this->getMockBuilder("stdClass")
->setMethods(array("encodePassword"))
->disableOriginalConstructor()
->getMock();
$encoder->expects($this->once())
->method("encodePassword")
->with("test", "10")
->will($this->returnValue(md5("test10")));
$account_bm->setEncoder($encoder);
$account_bm->expects($this->once())
->method("persistEntity")
->with($expectedAccount);
$account_bm->newUser($account);
}
但测试仍然失败 - 我会留给你替换错误的值,以检查你是否理解代码的工作原理;-)