Mocking Ldap objects¶
Unit-testing applications that use The Ldap Library can be troublesome.
The code under test may depend on ldap library’s
interfaces, such as SearchQueryInterface
.
A common practice for unit-testing is to not depend on real services, so we
can’t just use actual implementations, such as
ExtLdap\SearchQuery
,
which operate on real LDAP databases.
Mocking is a technique of replacing actual object with fake ones called mocks
or stubs. It’s applicable also to our case, but creating mocks for
objects/interfaces of The Ldap Library becomes complicated when it comes
to higher-level interfaces such as the
SearchQueryInterface
.
Consider the following function to be unit-tested
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function getPosixAccounts(SearchQueryInterface $query) : array
{
function isPosixAccount(ResultEntryInterface $entry) : bool
{
$attributes = $entry->getAttributes();
$objectclasses = array_map('strtolower', $attributes['objectclass'] ?? []);
return in_array('posixaccount', $objectclasses);
}
$result = $query->getResult();
$users = [];
foreach ($result->getResultEntryIterator() as $entry) {
if (isPosixAccount($entry)) {
$users[] = $entry;
}
}
return $users;
}
|
The expected behavior is that getPosixAccounts()
executes $query
and
extracts posixAccount entries from its result. From the code we see
immediately, that our unit-test needs to create a mock for
SearchQueryInterface
. The mock shall
provide getResult()
method returning an instance of
ResultInterface
(another mock?) having
getResultEntryIterator()
method that shall return an instance of
ResultEntryIteratorInterface
(yet another
mock?) and so on. Quite complicated as for single unit-test, isn’t it?
To facilitate unit-testing and mocking, The Ldap Library provides a bunch
of classes for “fake objects” under the Korowai\Lib\Ldap\Adapter\Mock
namespace. For the purpose of our example, an instance of
Result
class (from the above
namespace) may be created
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // this is used instead of a long chain of mocks...
$result = Result::make([
[
'dn' => 'cn=admin,dc=org',
'cn' => 'admin',
'objectClass' => ['person']
], [
'dn' => 'uid=jsmith,dc=org',
'uid' => 'jsmith',
'objectClass' => ['posixAccount']
], [
'dn' => 'uid=gbrown,dc=org',
'uid' => 'gbrown',
'objectClass' => ['posixAccount']
],
]);
|
and used as a return value of the mocked
SearchQueryInterface::getResult()
method.
1 2 3 4 5 6 | $queryMock = $this->getMockBuilder(SearchQueryInterface::class)
->getMockForAbstractClass();
$queryMock->expects($this->once())
->method('getResult')
->with()
->willReturn($result);
|
This significantly reduces the boilerplate of mocking the
SearchQueryInterface
(we created one mock
and one fake object instead of a chain of four mocks).
The full example is the following
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | /* [use] */
use PHPUnit\Framework\TestCase;
use Korowai\Lib\Ldap\Adapter\SearchQueryInterface;
use Korowai\Lib\Ldap\Adapter\ResultEntryInterface;
use Korowai\Lib\Ldap\Adapter\Mock\Result;
/* [/use] */
/* [functions] */
function getPosixAccounts(SearchQueryInterface $query) : array
{
function isPosixAccount(ResultEntryInterface $entry) : bool
{
$attributes = $entry->getAttributes();
$objectclasses = array_map('strtolower', $attributes['objectclass'] ?? []);
return in_array('posixaccount', $objectclasses);
}
$result = $query->getResult();
$users = [];
foreach ($result->getResultEntryIterator() as $entry) {
if (isPosixAccount($entry)) {
$users[] = $entry;
}
}
return $users;
}
/* [/functions] */
/* [testcase] */
class TestGetPosixAccounts extends TestCase
{
public function test()
{
/* [result] */
// this is used instead of a long chain of mocks...
$result = Result::make([
[
'dn' => 'cn=admin,dc=org',
'cn' => 'admin',
'objectClass' => ['person']
], [
'dn' => 'uid=jsmith,dc=org',
'uid' => 'jsmith',
'objectClass' => ['posixAccount']
], [
'dn' => 'uid=gbrown,dc=org',
'uid' => 'gbrown',
'objectClass' => ['posixAccount']
],
]);
/* [/result] */
/* [queryMock] */
$queryMock = $this->getMockBuilder(SearchQueryInterface::class)
->getMockForAbstractClass();
$queryMock->expects($this->once())
->method('getResult')
->with()
->willReturn($result);
/* [/queryMock] */
$entries = getPosixAccounts($queryMock);
$this->assertCount(2, $entries);
$this->assertInstanceOf(ResultEntryInterface::class, $entries[0]);
$this->assertInstanceOf(ResultEntryInterface::class, $entries[1]);
$this->assertSame('uid=jsmith,dc=org', $entries[0]->getDn());
$this->assertSame(['uid' => ['jsmith'], 'objectclass' => ['posixAccount']], $entries[0]->getAttributes());
$this->assertSame('uid=gbrown,dc=org', $entries[1]->getDn());
$this->assertSame(['uid' => ['gbrown'], 'objectclass' => ['posixAccount']], $entries[1]->getAttributes());
}
}
/* [/testcase] */
/* [test] */
$testCase = new TestGetPosixAccounts;
$testCase->test();
/* [/test] */
|
Predefined fake objects¶
The Result
object, used in previous
example, is an example of what we’ll call “fake objects”. A fake object is an
implementation of particular interface, which imitates actual implementation,
except the fake object does not call any actual LDAP implementation
(such as the PHP ldap extension). For example,
Result
implements ResultInterface
providing two iterators, one over a collection of
ResultEntry
objects and the other
over ResultReference
objects. Once
configured with arrays of entries and references, the
Result
, behaves
exactly as real implementation of
ResultInterface
would.
Below is a list of interfaces and their fake-object implementations.