第二节 处理依赖
在我们的例子中,解决依赖性问题的最干净方法是将User类与数据库访问和Mail类用法分开。 逻辑是User类是一个实体,但是我们将有第二个UserManager类,它允许我们在数据库中持久存储(存储)User类的对象。 要测试User类,我们将使用单元测试,并测试UserManager,我们将使用集成测试。 在我们的例子中,我们将sendActivationEmail()和createUser()移动到UserManager类。
然后,User类成为一个轻量级类,如以下代码段所示:
<?php namespace Application; /** * Class User * @package Application */ class User { public $userId; public $firstName; public $lastName; public $email; public $password; public $salt; /** * @param array $options */ public function __construct ( array $options ) { foreach ($options as $key => $value) { if (property_exists( $this, $key )) { $this->{$key} = $value; } } } /** * validates properties * @return bool */ public function isInputValid () { if (empty( $this->firstName ) || empty( $this->lastName ) || empty( $this->email ) || empty( $this->password ) || !filter_var( $this->email, FILTER_VALIDATE_EMAIL )) { return false; } else { return true; } } /** * creates password hash */ public function createPassword () { $this->salt = substr( str_shuffle( "0123456789abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ" ), 0, 15 ); $this->password = sha1( $this->password . $this->salt ); } /** * verifies password * @param string $password * @return bool */ public function verifyPassword ( $password ) { return ( $this->password === sha1( $password . $this->salt ) ); } }
首先,让我们看看如何为User类编写测试,我们决定进行一些重构,并将sendActivationEmail()和createUser()移动到UserManager类,如下面的代码片段所示:
<?php namespace ApplicationTest; use Application\User; require_once 'User.php'; class UserTest extends \PHPUnit_Framework_TestCase { /** * @var \Application\User */ private $user; public function setUp () { $this->user = new User( array ( 'firstName' => 'FirstName', 'lastName' => 'LastName', 'email' => 'example@test.com', 'password' => 'password123' ) ); } public function testValidInput () { $this->assertTrue( $this->user->isInputValid() ); $this->user->email = null; $this->assertFalse( $this->user->isInputValid() ); } public function testInValidInput () { $this->user->email = null; $this->assertFalse( $this->user->isInputValid() ); } public function testCreatedPassword () { $this->user->createPassword(); $this->assertEquals( sha1( 'password123' . $this->user->salt ), $this->user->password ); $this->assertNotEquals( sha1( null ), $this->user->password ); } public function testEmptyPassword () { $this->user->createPassword(); $this->assertNotEquals( sha1( null ), $this->user->password ); } public function testValidPassword () { $this->user->createPassword(); $this->assertTrue( $this->user->verifyPassword( 'password123' ) ); } public function testInvalidPassword () { $this->user->createPassword(); $this->assertFalse( $this->user->verifyPassword( null ) ); } }
在IDE中执行测试时,您应该看到类似的输出,如以下屏幕截图所示:
通过这种方式,我们只编写和运行单元测试,并且报告的此类代码覆盖率为95%。 重要的是不仅要测试预期结果,例如assertTrue(),还要传递无效参数并验证assertFalse(),以确保代码能很好地处理所有场景。
在这种情况下,我们正在测试User,每个测试都需要用户对象。 setUp()方法将在第6章“测试隔离和测试交互”中进行深入描述,用于为每个测试创建用户对象,因此不必复制代码。 UserManager类包含sendActivationEmail()和createUser()方法,这些方法连接到数据库并发送电子邮件。 区别在于所需的依赖项,电子邮件,数据库和配置对象在构造函数中传递,如以下代码段所示:
<?php namespace Application; class UserManager { private $db; private $email; private $config; public function __construct ( \Util\Mail $email, \PDO $db, $config ) { $this->email = $email; $this->db = $db; $this->config = $config; } /** * sends activation email */ private function sendActivationEmail ( \Application\User $user ) { $this->email->setEmailFrom( $this->config->email ); $this->email->setEmailTo( $user->email ); $this->email->setTitle( 'Your account has been activated' ); $this->email->setBody( "Dear {$user->firstName}\n Your account has been activated\n Please visit {$this->config->site_url}\n Thank you" ); $this->email->send(); } /** * @param User $user * @return bool */ public function createUser ( \Application\User $user ) { if (!$user->isInputValid()) { throw new \InvalidArgumentException( 'Invalid user data' ); } $user->createPassword(); /* @var $this ->db \PDO */ $sql = "INSERT INTO users(firstname, lastname, email, password, salt) VALUES (:firstname, :lastname, :email, :password, :salt)"; $statement = $this->db->prepare( $sql ); $statement->bindParam( ':firstname', $user->firstName ); $statement->bindParam( ':lastname', $user->lastName ); $statement->bindParam( ':email', $user->email ); $statement->bindParam( ':password', $user->password ); $statement->bindParam( ':salt', $user->salt ); if ($statement->execute()) { $user->userId = $this->db->lastInsertId(); $this->sendActivationEmail( $user ); return true; } else { throw new \Exception( 'User wasn\'t saved: ' . implode( ':', $statement->errorInfo() ) ); } return false; } }
要测试代码,我们有以下两个选项:
- 单元测试:验证核心功能和隔离地演练一段代码
- 集成测试:这将验证与其他组件的交互
它们都很重要。 如果沿着单元测试路线走下去,可能很难验证我们是否可以真正存储/检索数据库中的数据。 在这种情况下,这将是一个问题。 对于电子邮件,假设我们并不担心发送电子邮件; 我们有Mail类的测试,我们不想在这里测试它。 要获得完整的图片,以下代码段显示了Mail类的框架:
<?php namespace Util; class Mail { public function setEmailFrom($emailFrom) {} public function setEmailTo($emailTo) {} public function setTitle($title) {} public function setBody($body) {} public function send() {} }
以下代码段显示了UserManager类的测试:
<?php namespace ApplicationTest; use Application\UserManager; use Application\User; require_once 'User.php'; require_once 'UserManager.php'; require_once 'Mail.php'; class UserManagerTest extends \PHPUnit_Framework_TestCase { public function testCreateUser () { $db = new \PDO( 'mysql:host=localhost;port=3306; dbname=test', 'root', '' ); $config = new \stdClass(); $config->email = 'test@example.com'; $config->site_url = 'http://example.com'; $user = new User( array ( 'firstName' => 'FirtsName', 'lastName' => 'LastName', 'email' => 'user@example.com', 'password' => 'password123' ) ); $email = $this->getMock( '\Util\Mail' ); $userManager = new UserManager( $email, $db, $config ); $this->assertTrue( $userManager->createUser( $user ) ); $this->assertEquals( sha1( 'password123' . $user->salt ), $user->password ); $this->assertTrue( $user->userId > 0 ); } }
此代码说明了如何以更好的方式处理依赖项。 我们将在构造函数中传递全局变量($ db,$ email和$ config),或者如果您愿意,可以使用setter设置它们,而不是使用全局和会话变量或在代码中使用硬编码类。 在您的应用程序中,您可以使用依赖注入等技术自动将所需的依赖项传递到您的类中。 对于测试,能够传递这些对象的自定义版本是一个优点。 例如,我们使用PDO(MySQL),我们连接到MySQL数据库,但是使用PDO,您可以使用SQLite数据库PDO(使用'sqlite:/tmp/myDB.db')并只使用本地数据库。 我们将在第9章数据库测试中看到如何与数据库交互。 目前,它仅作为如何处理所需数据库连接的示例显示。
对于Mail类,我们使用了另一种技巧。 由于我们对发送电子邮件不感兴趣,我们使用PHPUnit getMock()创建了一个虚拟对象电子邮件,如下面的代码行所示:
$email = $this->getMock('\Util\Mail')
该对象具有所有Mail类方法但没有实现; 这只是返回NULL。 对我们来说,没关系。 代码有效,我们不希望在这里发送任何电子邮件。 有关这些技术的更多信息将在第8章“使用测试双打”中讨论。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步