简体   繁体   English

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

[英]how to mock php native ldap fuctions like lda_connect, ldap_get_entries, ldap_search and ldap_read

I have the below class to connect to ldap server to check whether the user belongs to a group or not. 我有下面的类连接到ldap服务器,以检查用户是否属于一个组。 i want to write unit test for this class. 我想为此课程写单元测试。 how do i achieve this. 我该如何做到这一点。 how to mock the ldap php native functions. 如何模拟ldap php本机函数。 can some one please help me with a bit of sample code. 有人可以帮我提供一些示例代码吗?

<?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;
}

Take a look at uopz - this package allows you to override native functions in PHP. 看一下uopz-此软件包允许您覆盖PHP中的本机函数。

Example of usage below (refer to the docs on Github as there are changes between PHP 5 and 7). 下面的用法示例(请参阅Github上的文档,因为PHP 5和7之间有变化)。

uopz-2.0.x 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 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');

Note: be careful of which version you're installing. 注意:请注意要安装的版本。 >=5.x supports PHP 7.x and <=2.0.x supports PHP 5.3.x >=5.x支持PHP 7.x,而<=2.0.x支持PHP 5.3.x

You normally do not need to mock these functions. 您通常不需要模拟这些功能。 Mocking these would in fact mean you would mimic server behavior which can be cumbersome. 模拟这些实际上意味着您将模仿服务器的行为,这可能很麻烦。 You might have a flaw in your implementation that does not work with a specific ldap server (setup) -or- you have a flaw in using your LdapAuthentication class. 您的实现中可能存在与特定的ldap服务器(安装程序)不兼容的缺陷-或者-您在使用LdapAuthentication类时存在缺陷。

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

That is also because the LdapAuthentication is a wrapper of the PHP Ldap extension - which is good practice as you shield the rest of your application away from the concrete library so you can change it over time (eg handle some differences in setups and changes over time). 这也是因为LdapAuthentication是PHP Ldap扩展程序的包装-这是一个好习惯,因为您可以将应用程序的其余部分与具体库隔离开,以便随时间进行更改(例如,处理设置中的某些差异以及随时间的变化) )。

One strategy to handle this is interface testing . 解决这一问题的一种策略是接口测试 The LdapAuthentication has a public interface you use in your application. LdapAuthentication具有您在应用程序中使用的公共接口。 To test if your application interfaces correctly with the type is to mock that class itself. 要测试您的应用程序是否与类型正确连接,就是要模拟该类本身。 This will cover the public interface of it. 这将覆盖它的公共接口。

On the other hand you would like to test if the authenticator works with an ldap server instance. 另一方面,您想测试身份验证器是否与ldap服务器实例一起使用。 This requires a test-suite that all functionality the public interface represents is available on a concrete server. 这要求测试套件在特定服务器上提供公共接口表示的所有功能。 This is effectively testing the server interface . 这有效地测试了服务器接口 In Phpunit this could be two test-cases. 在Phpunit中,这可能是两个测试用例。 One for the public class interface and one to test the integration for a (test-) ldap-server configuration. 一个用于公共类接口,另一个用于测试(测试)ldap-server配置的集成。

Two interfaces to test: 有两个要测试的接口:

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

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

From the viewpoint of your application, mocking the PHP internal functions (via the PHP Ldap extension) should not be necessary to test your application. 从您的应用程序的角度来看,模拟PHP内部功能(通过PHP Ldap扩展名)对于测试您的应用程序不是必需的。

To test the Ldap server interface, you do not need to mock these functions as well, as in that case, you actually want to test if things work for real, not in a mock. 测试LDAP服务器的界面,你不必嘲笑这些功能以及,在这种情况下,你真的想测试,如果事情真正的工作,而不是在一个模拟。 So from that interface test, you even don't want to mock these internal functions, you actually want to use them. 因此,从该接口测试开始,您甚至不想模拟这些内部函数,而实际上想要使用它们。

As mocking a class that represents server interaction can be complex and setting up mock objects dynamically for such interaction intensive types can be cumbersome, you should consider to write the mock object of the LdapAuthentication in plain PHP encoding the brief functionality you expect that class to cover with example data. 由于模拟表示服务器交互的类可能很复杂,并且为此类交互密集型类型动态地设置模拟对象可能很麻烦,因此您应该考虑使用纯PHP编写LdapAuthentication的模拟对象,对简单的功能进行编码,希望该类涵盖与示例数据。

That is, you write a second implementation of the LdapAuthentication , lets name it LdapAuthenticationMock which behaves as you expect it. 也就是说,您编写了LdapAuthentication的第二种实现,将其命名为LdapAuthenticationMock ,它的行为与您期望的一样。 It is common to place it next to the unit-test, that is in the same directory as LdapAuthenticationTest (the unit test-case). 通常将其放置在与LdapAuthenticationTest (单元测试用例)相同的目录中的单元测试旁边。

But before you can create such a mock that fulfills the public interface of LdapAuthentication there is some refactoring work to do first. 但是在创建可以满足LdapAuthentication的公共接口的模拟之前,首先需要进行一些重构工作。 The good part of it is, that your overall application will benefit from it, not only testing. 它的好处是,您的整个应用程序将从中受益,而不仅仅是测试。

That extra work is to extract the interface from LdapAuthentication . 额外的工作是从LdapAuthentication中 提取接口 Extracting an interface means that you create an interface (see PHP interfaces) with the same name containing those public methods you make use of in your application: 提取接口意味着您将创建一个具有相同名称的接口(请参阅PHP接口),其中包含您在应用程序中使用的那些公共方法:

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

Now as you can't have interfaces and classes share the same name, you rename your LdapAuthentication class to a different name implementing the interface: 现在,由于接口和类不能共享相同的名称,因此可以将LdapAuthentication类重命名为实现该接口的其他名称:

Before: 之前:

class LdapAuthentication
{
    ...

After: 后:

class FooLdapAuthentication implements LdapAuthentication
{
    ...

(Please note that the name is just exemplary, you should be able to find a better fit) (请注意,该名称仅是示例性名称,您应该可以找到更合适的名称)

The main benefit of this is that you're now able to program against the interface instead of the concrete type. 这样做的主要好处是您现在可以针对接口而不是具体类型进行编程。 If you name the interface like you named the class in your current code (so to say how you named it earlier) your existing code automatically changes from programming against the concrete type to programming against the interface . 如果您像在当前代码中为类命名那样来命名接口(可以说是您之前的命名方式),则现有代码会自动从针对具体类型的编程变为针对接口的编程 For example, type-hints now accept any implementation. 例如,类型提示现在可以接受任何实现。

This allows you to swap the implementation (that is, you can make changes in subclasses or re-write an implementation even in a new class, for example one that uses a different ldap library or is for a different kind of ldap server w/o breaking the rest of your code). 这使您可以交换实现(即,您可以在子类中进行更改或甚至在新类中也可以重写实现,例如,使用不同的ldap库或用于另一种类型的ldap服务器而没有)的实现。破坏其余的代码)。

One such new implementation for example will become the LdapAuthenticationMock : 例如,一种这样的新实现将成为LdapAuthenticationMock

class LdapAuthenticationMock implements LdapAuthentication
{
    ...

You can then pass it around in tests to test your application code w/o even requiring the ldap server or even that ldap PHP extension. 然后,您可以在测试中传递它来测试您的应用程序代码,而无需ldap服务器甚至 ldap PHP扩展。 Thanks to the interface, PHP will notify you, if you have not implemented it in full. 多亏了该接口,如果您尚未完全实现PHP,PHP会通知您。

Next to the test of the mock (which works more that you write down how you expect that class to work, often tests is writing down specification in code-near manner) you also need an integration test of the concrete implementation that you have against the Ldap (test-) server, FooLdapAuthenticationTest . 在模拟测试的旁边(可以使您写下类期望的工作方式,而通常情况下,测试会以接近代码的方式写下规范),您还需要针对具体实现的集成测试。 Ldap(测试)服务器FooLdapAuthenticationTest

Writing these tests will help you to write the Ldap authentication in isolated test-cases w/o runing your whole application. 编写这些测试将帮助您在独立的测试用例中编写Ldap身份验证,而无需运行整个应用程序。 The application then - thanks to programming against interfaces - can be written w/o caring any longer about the implementation details of FooLdapAuthentication or any other LdapAuthentication . 然后,由于对接口进行了编程,因此可以编写该应用程序而不再关心FooLdapAuthentication或任何其他LdapAuthentication的实现细节。

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

So with interface testing you can test both your application classes -or- the server and you can make changes at the place where they belong to (or handle changes in the server interface w/o the need to change the whole application). 因此,通过接口测试,您可以测试应用程序类或服务器,也可以在它们所属的位置进行更改(或在服务器接口中进行更改,而无需更改整个应用程序)。

Having the mock as concrete PHP class also has the benefit that you do not need to setup it intensively again and again for other tests that need to collaborate with an LdapAuthentication , you just inject that mock. 将模拟作为具体的PHP类还具有以下好处:您无需为需要与LdapAuthentication协作的其他测试而一次又一次地密集设置它,只需注入该模拟即可。

If you setup the autoloader properly this is rather straight forward to use. 如果正确设置自动装带器,则很容易使用。 No cumbersome mocking of internal functions, easy to write unit tests that need an LdapAuthentication to work with and extensive integration testing of the server interface is possible this way. 无需繁琐的内部功能模拟 ,无需LdapAuthentication即可编写的易于编写的单元测试,并且可以通过这种方式对服务器接口进行广泛的集成测试。

This is one way to overcome the problems that integration tests create: 这是克服集成测试所产生问题的一种方法:

  • Coverage: Integration tests often lack behind (covered via the PHP interface, any implementation needs to fulfill the interface 100% otherwise it won't start) 覆盖范围:通常缺乏集成测试(通过PHP接口发现,任何实现都需要100%满足该接口,否则它将无法启动)

  • Complexity: Integration tests do not show the exact cause of error (you notice a server problem somewhere in your application code while the application code just expects your implementation to do the work instead of erroring out) 复杂性:集成测试不能显示错误的确切原因(您在应用程序代码中的某个地方注意到服务器问题,而应用程序代码只是希望您的实现能够完成工作而不是出错)

In a more advanced test-setup, the whole server interface on the level of the network protocol would be abstracted as well so that you can test a concrete server it it would match the expected interface in both ways in and out. 在更高级的测试设置中,还将抽象化网络协议级别的整个服务器接口,以便您可以测试具体的服务器,使其在输入和输出方面都与预期的接口匹配。 Same for the client side, again in and out. 客户端也一样,再次进出。 However this would certainly leave the scope of PHP here. 但是,这肯定会超出PHP的范围。 With my suggestion given, you at least separate your application from the server interface by introducing a PHP interface in between. 根据我的建议,您至少可以通过在两者之间引入PHP接口来将应用程序与服务器接口分离。 You would still have an integration test for a concrete (Test-) server so the overall integration test problem is reduced and limited to a (more concrete) integration test-case. 您仍将针对具体的(Test-)服务器进行集成测试,因此可以减少总体集成测试问题,并将其限制为(更具体的)集成测试用例。 As you're often dependent on a more or less concrete server, I think this shortcut is ok until you discover further (regression) testing needs. 由于您通常依赖于或多或少的具体服务器,因此我认为在您发现更多(回归)测试需求之前,可以使用此快捷方式。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM