Smelly Code: Direct Object Instantiation as a Testability Killer
3 min read
3 min read
This Smelly Code post discusses mainly a testability issue without going into further detail on other things like readability which could be improved for sure as well.
Original, smelly codepublic Account ReadCompleteAccountByADUsernameAndServiceUID(string adUsername, string serviceInstanceUID)
{
IList<Address>> addresses;
IList<Contact> contacts;
MasterDataBL masterDataBL = new MasterDataBL();
Account result = AccoDao.ReadCompleteAccountByADUsernameAndServiceUID(adUsername, serviceInstanceUID, ConnectionString.Master, out addresses, out contacts);
result.PhoneNumber = contacts.Where(x => x.ContactType.Id == masterDataBL.GetCachedContactTypeByUid(ContactType.WellKnownUid.PHONE).Id).FirstOrDefault();
result.MobilePhone = contacts.Where(x => x.ContactType.Id == masterDataBL.GetCachedContactTypeByUid(ContactType.WellKnownUid.MOBILE).Id).FirstOrDefault();
result.Residence = addresses.Where(x => x.AddressTypeId == masterDataBL.GetCachedAddressTypeByUid(AddressType.WellKnownUid.RESIDENCE).Id).FirstOrDefault();
result.Domicile = addresses.Where(x => x.AddressTypeId == masterDataBL.GetCachedAddressTypeByUid(AddressType.WellKnownUid.DOMICILE).Id).FirstOrDefault();
return result;
}
MasterDataBL
class in line 6. The problem
with this declaration is that it makes testing extremely difficult. When you're writing a proper unit test, then your
ultimate goal is to take that unit under test out of its context and test it in complete isolation.
What does this mean? Taking an object out of its context means to remove its dependencies to its neighbors. In this
example the method ReadCompleteAccountByADUsernameAndServiceUID(...)
is a method of my class AccountBL
.
Obviously that class collaborates with others, creating dependencies. Hence, they are inevitable, however, there is a
distinction between the kind of coupling that is created: tight vs. loose coupling. I've written a blog
post about that about half a year ago.MasterDataBL
with an appropriate stub. Consequently, if MasterDataBL
is instantiated inside the method and then executes a call which ends in the database, you won't be able to prevent
that.private IMasterDataBl _MasterDataBL;
public IMasterDataBl MasterDataBL
{
get
{
if (_MasterDataBL == null)
_MasterDataBL = BlAgentConfigurationSection.GetAgent().Get<IMasterDataBl>();
return _MasterDataBL;
}
set
{
_MasterDataBL = value;
}
}
public Account ReadCompleteAccountByADUsernameAndServiceUID(string adUsername, string serviceInstanceUID)
{
IList<Address> addresses;
IList<Contact> contacts;
Account result = AccoDao.ReadCompleteAccountByADUsernameAndServiceUID(adUsername, serviceInstanceUID, ConnectionString.Master, out addresses, out contacts);
result.PhoneNumber = contacts.Where(x => x.ContactType.Id == MasterDataBL.GetCachedContactTypeByUid(ContactType.WellKnownUid.PHONE).Id).FirstOrDefault();
result.MobilePhone = contacts.Where(x => x.ContactType.Id == MasterDataBL.GetCachedContactTypeByUid(ContactType.WellKnownUid.MOBILE).Id).FirstOrDefault();
result.Residence = addresses.Where(x => x.AddressTypeId == MasterDataBL.GetCachedAddressTypeByUid(AddressType.WellKnownUid.RESIDENCE).Id).FirstOrDefault();
result.Domicile = addresses.Where(x => x.AddressTypeId == MasterDataBL.GetCachedAddressTypeByUid(AddressType.WellKnownUid.DOMICILE).Id).FirstOrDefault();
return result;
}
IMasterDataBl
(note how relying on interfaces decouples things). Moreover I've wrapped everything inside a property which allows me
to explicitly set the instance of the used MasterDataBL. You guessed it, that's exactly the point where my
test injects its stub while during production code the factory will be invoked.