如何通过 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);
}

但测试仍然失败 - 我会留给你替换错误的值,以检查你是否理解代码的工作原理;-)