I'm a sysadmin writing a tool to perform administrative tasks on our upcoming new account management system that runs over LDAP. I want the tool to be highly flexible, configurable, and reliable, so I'm using automated test-driven development. I've started writing a module to perform LDAP connections and commands, but the problem is that writing my unit tests for this module takes 90% of my time. As it stands at the moment, with only a couple of class methods implemented so far with four more on the drawing board, here is the module that I am testing:
import ldap
import ldap.sasl
SCOPE=ldap.SCOPE_SUBTREE # will be moved to configuration at a later point
class auth():
kerb, simple, noauth = range(3)
class LDAPObjectManager():
def __init__(self, uri, authtype, user=None, password=None, **kwargs):
self._ldo = ldap.initialize(uri)
for key, value in kwargs.items():
self._ldo.set_option(getattr(ldap, key), value)
if authtype == auth.simple:
self._ldo.simple_bind_s(user, password)
elif authtype == auth.kerb:
self._ldo.sasl_interactive_bind_s('', ldap.sasl.gssapi())
def _stripReferences(self, ldif):
return filter(lambda x: x[0] is not None, ldif)
def gets(self, sbase, sfilter):
ldif = self._ldo.search_ext_s(sbase, SCOPE, sfilter)
result = self._stripReferences(ldif)
if not result:
raise RuntimeError("""No results found for single-object query:
base: %s
filter: %s""" %(sbase, sfilter))
if len(result) > 1:
raise RuntimeError("""Too many results found for single-object \
query:
base: %s
filter: %s
results: %s""" %(sbase, sfilter, [r[0] for r in result]))
return result[0]
def getm(self, sbase, sfilter):
return self._stripReferences(self._ldo.search_ext_s(sbase, SCOPE,
sfilter))
Here are my unit tests that I've written so far:
import mock
import unittest
import src.ldapobjectmanager
@mock.patch('src.ldapobjectmanager.ldap', autospec=True)
class TestLOMInitializationAndOptions(unittest.TestCase):
def testAuth(self, mock_ldap):
uri = 'ldaps://foo.bar:636'
def getNewLDOandLOM(auth, **kwargs):
ldo = mock_ldap.ldapobject.LDAPObject(uri)
mock_ldap.initialize.return_value = ldo
lom = src.ldapobjectmanager.LDAPObjectManager(uri, auth, **kwargs)
return ldo, lom
# no auth
ldo, lom = getNewLDOandLOM(src.ldapobjectmanager.auth.noauth)
self.assertEqual(ldo.simple_bind_s.call_args_list, [])
self.assertEqual(ldo.sasl_interactive_bind_s.call_args_list, [])
# simple auth
user = 'foo'
password = 'bar'
ldo, lom = getNewLDOandLOM(src.ldapobjectmanager.auth.simple,
user=user, password=password)
self.assertEqual(ldo.simple_bind_s.call_args_list,
[((user, password),)])
# kerb auth
sasl = mock.MagicMock()
mock_ldap.sasl.gssapi.return_value = sasl
ldo, lom = getNewLDOandLOM(src.ldapobjectmanager.auth.kerb)
self.assertEqual(ldo.sasl_interactive_bind_s.call_args_list,
[(('', sasl),)])
def testOptions(self, mock_ldap):
uri = 'ldaps://foo.bar:636'
def addOption(**kwargs):
ldo = mock_ldap.ldapobject.LDAPObject(uri)
mock_ldap.initialize.return_value = ldo
for key, value in kwargs.items():
if not hasattr(mock_ldap, key):
with self.assertRaises(AttributeError):
lom = src.ldapobjectmanager.LDAPObjectManager(uri,
src.ldapobjectmanager.auth.noauth, **{key:value})
else:
lom = src.ldapobjectmanager.LDAPObjectManager(uri,
src.ldapobjectmanager.auth.noauth, **{key:value})
self.assertEqual(ldo.set_option.call_args,
((getattr(mock_ldap, key), value),))
addOption(OPT_X_TLS=1, OPT_BOGUS=1, OPT_URI="ldaps://baz.bar")
@mock.patch('src.ldapobjectmanager.ldap', autospec=True)
class TestLOMGetMethods(unittest.TestCase):
def testGets(self, mock_ldap):
uri = 'ldaps://foo.bar:636'
ldo = mock_ldap.ldapobject.LDAPObject(uri)
mock_ldap.initialize.return_value = ldo
lom = src.ldapobjectmanager.LDAPObjectManager(uri,
src.ldapobjectmanager.auth.kerb)
# if gets() fails to find an object, it should throw an exception
ldo.search_ext_s.return_value = []
with self.assertRaises(RuntimeError) as err:
lom.gets("", "")
# sometimes references are included in the result
# these have no DN and should be discarded from the result
ldo.search_ext_s.return_value = [(None, ['ldaps://foo.bar/cn=ref'])]
with self.assertRaises(RuntimeError) as err:
lom.gets("", "")
# if gets() finds > 1 object, it should throw an exception
ldo.search_ext_s.return_value = [
('CN=fred,OU=People,DC=foo,DC=bar', {'name': ['fred']}),
('CN=george,OU=People,DC=foo,DC=bar', {'name': ['george']})
]
with self.assertRaises(RuntimeError) as err:
lom.gets("", "(|(name=fred)(name=george))")
# if gets() finds exactly 1 object, it should return that object
expectedresult = ('CN=alice,OU=People,DC=foo,DC=bar', {'name': ['alice']})
ldo.search_ext_s.return_value = [expectedresult]
actualresult = lom.gets("", "name=alice")
self.assertEqual(expectedresult, actualresult)
# repeat with reference in result
expectedresult = ('CN=alice,OU=People,DC=foo,DC=bar', {'name': ['alice']})
ldo.search_ext_s.return_value = [expectedresult,
(None, ['ldaps://foo.bar/cn=ref'])]
actualresult = lom.gets("", "name=alice")
self.assertEqual(expectedresult, actualresult)
def testGetm(self, mock_ldap):
uri = 'ldaps://foo.bar:636'
ldo = mock_ldap.ldapobject.LDAPObject(uri)
mock_ldap.initialize.return_value = ldo
lom = src.ldapobjectmanager.LDAPObjectManager(uri,
src.ldapobjectmanager.auth.kerb)
expectedresult = [
('CN=fred,OU=People,DC=foo,DC=bar', {'name': ['fred']}),
('CN=george,OU=People,DC=foo,DC=bar', {'name': ['george']})
]
ldo.search_ext_s.return_value = expectedresult
actualresult = lom.getm("", "(|(name=fred)(name=george))")
self.assertEqual(expectedresult, actualresult)
# repeat with reference in result
alice = ('CN=alice,OU=People,DC=foo,DC=bar', {'name': ['alice']})
reference = (None, ['ldaps://foo.bar/cn=ref'])
ldo.search_ext_s.return_value = [reference, alice, alice, reference,
reference, alice, alice, reference, alice, reference]
actualresult = lom.getm("", "name=alice")
self.assertEqual([alice, alice, alice, alice, alice], actualresult)
Here are the problems that I perceive with my testing code:
- My unit tests are huge and cumbersome to work with
- Because of the need to mock out the LDAP library, my unit tests are tightly coupled with the module implementation
- The mock makes it difficult to factor out common code in my unit tests
- Because of the above two bullet points, it takes much longer to write & debug my unit tests than it does to write my module, and I don't feel like I'm getting much useful coverage from my unit tests other than confirming the way I call the LDAP library.
How do I address these problems?
One potential cause for these problems is that I'm simply writing a wrapper around the LDAP library that does too little, but I haven't been able to sketch out my project in a way that avoids the need for such a wrapper or module.