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.

Fake Objects
Interface Fake Object
ResultInterface Result
ResultEntryInterface ResultEntry
ResultReferenceInterface ResultReference
ResultEntryIteratorInterface ResultEntryIterator
ResultReferenceIteratorInterface ResultReferenceIterator
ResultAttributeIteratorInterface ResultAttributeIterator
ResultReferralIteratorInterface ResultReferralIterator