如何模拟php本机ldap功能,如lda_connect,ldap_get_entries,ldap_search和ldap_read

时间:2017-06-27 17:25:27

标签: php unit-testing mocking ldap phpunit

我有以下类连接到ldap服务器以检查用户是否属于某个组。我想为这堂课写单元测试。我该如何实现这一目标。如何模拟ldap php本机函数。有人可以帮我一些示例代码。

<?php

namespace ABC\Admin\Login;

use Doctrine\ORM\EntityManagerInterface;

/**
 * Class Authenticate AD Login
 * @package Adp\Admin\Login
 */
class LdapAuthentication
{

/**
 * @var string host
 */
private $ldapHost;

/**
 * @var string Admin
 */
private $ldapDomain;

/**
 * @var string DN
 */
private $baseDn;

/**
 * @var EntityManagerInterface
 */
private $entityManager;

public function validateUser($user, $password)
{

    $ad = ldap_connect("$this->ldapHost") or die('Could not connect to LDAP server.');
    ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);
    ldap_set_option($ad, LDAP_OPT_REFERRALS, 0);
    if (!ldap_bind($ad, "{$user}@{$this->ldapDomain}", $password)) {
        return false;
    }
    $userDn = $this->getDN($ad, $user, $this->baseDn);

    return $this->checkGroupEx($ad, $userDn, $groups);
    ldap_unbind($ad);

}


/**
 * @param $ad
 * @param $samAccountName
 * @param $baseDn
 * @return string
 */
private function getDN($ad, $samAccountName, $baseDn)
{
    $attributes = array('dn');
    $result = ldap_search(
        $ad,
        $baseDn,
        "(samaccountname={$samAccountName})",
        $attributes
    );
    if ($result === false) {
        return '';
    }
    $entries = ldap_get_entries($ad, $result);
    if ($entries['count'] > 0) {
        return $entries[0]['dn'];
    }

    return '';
}

/**
 * @param $ad
 * @param $userDn
 * @param $groups
 * @param $roles
 * @return bool
 */
private function checkGroupEx($ad, $userDn, $groups)
{
    $attributes = array('cn','memberof', 'mail');
    $result = ldap_read($ad, $userDn, '(objectclass=*)', $attributes);
    if ($result === false) {
        return false;
    };
    $entries = ldap_get_entries($ad, $result);
    $response = array();
    $name = explode(',', $entries[0]['cn'][0]);
    $response['firstName'] = $name[0];
    $response['lastName'] = $name[1];
    $response['email'] = $entries[0]['mail'][0];

    if ($entries['count'] <= 0) {
        return false;
    };
    if (empty($entries[0]['memberof'])) {
        return false;
    }
    for ($i = 0; $i < $entries[0]['memberof']['count']; $i++) {
        $groupName = explode(',', $entries[0]['memberof'][$i]);
        $pos = array_search(strtolower(substr($groupName[0], 3)), $groups);
        if (!empty($pos)) {

            return $response;
        }
    }
    return false;
}

/**
 * @return string
 */
public function getBaseDn()
{
    return $this->baseDn;
}

/**
 * @param string $baseDn
 */
public function setBaseDn($baseDn)
{
    $this->baseDn = $baseDn;
}

/**
 * @return string
 */
public function getLdapDomain()
{
    return $this->ldapDomain;
}

/**
 * @param string $ldapDomain
 */
public function setLdapDomain($ldapDomain)
{
    $this->ldapDomain = $ldapDomain;
}

/**
 * @return string
 */
public function getLdapHost()
{
    return $this->ldapHost;
}

/**
 * @param string $ldapHost
 */
public function setLdapHost($ldapHost)
{
    $this->ldapHost = $ldapHost;
}

2 个答案:

答案 0 :(得分:2)

看一下uopz - 这个包允许你覆盖PHP中的本机函数。

以下用法示例(请参阅Github上的文档,因为PHP 5和7之间存在更改)。

uopz-2.0.x的

// backup the original function
uopz_backup('ldap_connect');

// override and perform your testing
uopz_function('ldap_connect', function() {
    // override here
});
// testing...

// once finished
uopz_restore('ldap_connect');

uopz-5.0.x的

// override and perform your testing
uopz_set_return('ldap_connect', function() {
    // override here
});
// testing...

// reset return value
uopz_unset_return('ldap_connect');

注意:请注意您要安装的版本。 >=5.x支持PHP 7.x,<=2.0.x支持PHP 5.3.x

答案 1 :(得分:2)

您通常不需要模拟这些功能。嘲笑这些实际上意味着你会模仿服务器行为,这可能很麻烦。您的实现中可能存在一个缺陷,该缺陷不适用于特定的ldap服务器(设置) - 或者您在使用 LdapAuthentication 类时存在缺陷。

 your app  <--->  LdapAuthentication  <--->  ldap server

这也是因为 LdapAuthentication 是PHP Ldap扩展的包装器 - 这是一个很好的做法,因为您可以保护应用程序的其余部分远离具体库,以便您可以随时更改它(例如,处理设置和随时间变化的一些差异。)

处理此问题的一个策略是界面测试 LdapAuthentication 具有您在应用程序中使用的公共接口。要测试您的应用程序是否与该类型正确接口,请模拟该类本身。这将涵盖它的公共界面。

另一方面,您想测试身份验证器是否与ldap服务器实例一起使用。这需要一个测试套件,公共接口所代表的所有功能都可以在具体的服务器上使用。这有效地测试了服务器接口。在Phpunit中,这可能是两个测试用例。一个用于公共类接口,另一个用于测试(test-)ldap-server配置的集成。

要测试的两个接口:

application  <--->  <<ldap class interface>>

ldap class  <--->  <<ldap server interface>>

从您的应用程序的角度来看,测试应用程序不需要模拟PHP内部函数(通过PHP Ldap扩展)。

要测试Ldap服务器接口,你也需要模拟这些函数,因为在这种情况下,你实际上想要测试事情是否真实,而不是模拟。因此,通过该界面测试,您甚至不想模拟这些内部函数,您实际上想要使用它们。

由于模拟表示服务器交互的类可能很复杂,并且为此类交互密集型类型动态设置模拟对象可能很麻烦,您应该考虑在纯PHP中编写 LdapAuthentication 的模拟对象编码您期望该类用示例数据覆盖的简要功能。

也就是说,您编写 LdapAuthentication 的第二个实现,让它命名为 LdapAuthenticationMock ,其行为与您期望的一样。通常将它放在单元测试旁边,与 LdapAuthenticationTest (单元测试用例)在同一目录中。

但是在你创建这样一个满足 LdapAuthentication 公共接口的模拟之前,首先需要进行一些重构工作。其中很重要的是,您的整体应用程序将从中受益,而不仅仅是测试。

额外的工作是 LdapAuthentication 中提取界面。提取接口意味着您创建一个接口(请参阅PHP接口),其名称包含您在应用程序中使用的公共方法:

interface LdapAuthentication
{
    public function validateUser($user, $password);
    ...

现在,您可以将接口和类共享相同的名称,然后将 LdapAuthentication 类重命名为实现接口的其他名称:

在:

class LdapAuthentication
{
    ...

后:

class FooLdapAuthentication implements LdapAuthentication
{
    ...

(请注意,名称只是示例,您应该能够找到更合适的名称)

这样做的主要好处是您现在能够针对界面而不是具体类型进行编程。如果您将接口命名为您在当前代码中命名的类(以便说明您之前的命名方式),那么现有代码会自动从针对具体类型的编程更改为针对接口编程。例如,类型提示现在接受任何实现。

这允许你交换实现(也就是说,你可以在子类中进行更改或者甚至在新类中重写一个实现,例如使用不同的ldap库或者用于不同类型的ldap服务器的实现没有破坏你的其余代码。)

例如,一个这样的新实现将成为 LdapAuthenticationMock

class LdapAuthenticationMock implements LdapAuthentication
{
    ...

然后你可以在测试中传递它来测试你的应用程序代码,甚至不需要ldap服务器甚至那个 ldap PHP扩展。感谢界面,如果您没有完整实现,PHP将通知您。

除了mock的测试(更多的是你写下你希望该类如何工作,通常测试是以代码接近的方式写下规范),你还需要对你的具体实现进行集成测试反对Ldap(测试 - )服务器, FooLdapAuthenticationTest

编写这些测试将帮助您在运行整个应用程序的独立测试用例中编写Ldap身份验证。然后,应用程序可以编写针对接口的编程,无需再关注 FooLdapAuthentication 或任何其他 LdapAuthentication 的实现细节。

FooLdapAuthenticationTest.php  - test of the server interface
LdapAuthenticationTest.php     - test of the PHP interface
LdapAuthenticationMock.php     - mock object

因此,通过接口测试,您可以测试您的应用程序类 - 或者 - 服务器,您可以在它们所属的位置进行更改(或处理服务器接口中的更改,而无需更改整个应用程序)

将模拟作为具体的PHP类也有一个好处,即您不需要一次又一次地为需要与 LdapAuthentication 协作的其他测试进行密集设置,您只需注入该模拟。

如果正确设置自动装带器,则可以直接使用。不需要繁琐的内部函数模拟,易于编写需要 LdapAuthentication 进行单元测试的单元测试以及服务器接口的广泛集成测试。

这是克服集成测试创建的问题的一种方法:

  • 覆盖范围:集成测试经常缺乏(通过PHP界面覆盖,任何实现需要100%完成界面,否则它不会开始)

  • 复杂性:集成测试没有显示错误的确切原因(您发现应用程序代码中某处存在服务器问题,而应用程序代码只是希望您的实现执行工作而不是错误)

在更高级的测试设置中,网络协议级别上的整个服务器接口也将被抽象化,以便您可以测试具体的服务器,它将以预先接入的方式匹配预期的接口。同样适用于客户端,再次进出。但是,这肯定会留下PHP的范围。根据我的建议,您至少通过在其间引入PHP接口将您的应用程序与服务器接口分开。您仍然可以对具体(Test-)服务器进行集成测试,以便减少整体集成测试问题并限制为(更具体的)集成测试用例。由于您经常依赖于或多或少的具体服务器,我认为在您发现进一步(回归)测试需求之前,此快捷方式是可以的。