第二节 处理依赖

在我们的例子中,解决依赖性问题的最干净方法是将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章“使用测试双打”中讨论。

 

posted @ 2018-08-08 13:25  MysticGrrrr  阅读(247)  评论(0编辑  收藏  举报