Reputation: 243
I have the below class to connect to ldap server to check whether the user belongs to a group or not. i want to write unit test for this class. how do i achieve this. how to mock the ldap php native functions. 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;
}
Upvotes: 2
Views: 718
Reputation: 198203
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.
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 (e.g. handle some differences in setups and changes over time).
One strategy to handle this is interface testing. The LdapAuthentication has a public interface you use in your application. 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. 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. One for the public class interface and one to test the integration for a (test-) ldap-server configuration.
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.
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. 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.
That is, you write a second implementation of the LdapAuthentication, lets name it LdapAuthenticationMock which behaves as you expect it. It is common to place it next to the unit-test, that is in the same directory as LdapAuthenticationTest (the unit test-case).
But before you can create such a mock that fulfills the public interface of LdapAuthentication there is some refactoring work to do first. 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. 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:
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:
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).
One such new implementation for example will become the 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. Thanks to the interface, PHP will notify you, if you have not implemented it in full.
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.
Writing these tests will help you to write the Ldap authentication in isolated test-cases w/o runing your whole application. 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.
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.
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.
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)
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. With my suggestion given, you at least separate your application from the server interface by introducing a PHP interface in between. 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. 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.
Upvotes: 2
Reputation: 16591
Take a look at uopz - this package allows you to override native functions in PHP.
Example of usage below (refer to the docs on Github as there are changes between PHP 5 and 7).
// 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');
// 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
Upvotes: 2