I already blogged about the topic of validating conventions in the past (here and here). Doing this has been a fantastic way of keeping consistency across codebases I’ve worked on, and several of my colleagues at Readify adopted this approach with great success.
Recently I found myself using this approach even more often and in scenarios I didn’t think about initially. Take this two small tests I wrote today for Windsor.
[TestFixture]
public class ConventionVerification
{
[Test]
public void Obsolete_members_of_kernel_are_in_sync()
{
var message = new StringBuilder();
var kernelMap = typeof(DefaultKernel).GetInterfaceMap(typeof(IKernel));
for (var i = 0; i < kernelMap.TargetMethods.Length; i++)
{
var interfaceMethod = kernelMap.InterfaceMethods[i];
var classMethod = kernelMap.TargetMethods[i];
Scan(interfaceMethod, classMethod, message);
}
Assert.IsEmpty(message.ToString(), message.ToString());
}
[Test]
public void Obsolete_members_of_windsor_are_in_sync()
{
var message = new StringBuilder();
var kernelMap = typeof(WindsorContainer).GetInterfaceMap(typeof(IWindsorContainer));
for (var i = 0; i < kernelMap.TargetMethods.Length; i++)
{
var interfaceMethod = kernelMap.InterfaceMethods[i];
var classMethod = kernelMap.TargetMethods[i];
Scan(interfaceMethod, classMethod, message);
}
Assert.IsEmpty(message.ToString(), message.ToString());
}
private void Scan(MethodInfo interfaceMethod, MethodInfo classMethod, StringBuilder message)
{
var obsolete = EnsureBothHave<ObsoleteAttribute>(interfaceMethod, classMethod, message);
if (obsolete.Item3)
{
if (obsolete.Item1.IsError != obsolete.Item2.IsError)
{
message.AppendLine(string.Format("Different error levels for {0}", interfaceMethod));
}
if (obsolete.Item1.Message != obsolete.Item2.Message)
{
message.AppendLine(string.Format("Different message for {0}", interfaceMethod));
message.AppendLine(string.Format("\t interface: {0}", obsolete.Item1.Message));
message.AppendLine(string.Format("\t class : {0}", obsolete.Item2.Message));
}
}
else
{
return;
}
var browsable = EnsureBothHave<EditorBrowsableAttribute>(interfaceMethod, classMethod, message);
{
if (browsable.Item3 == false)
{
message.AppendLine(string.Format("EditorBrowsable not applied to {0}", interfaceMethod));
return;
}
if (browsable.Item1.State != browsable.Item2.State || browsable.Item2.State != EditorBrowsableState.Never)
{
message.AppendLine(string.Format("Different/wrong browsable states for {0}", interfaceMethod));
}
}
}
private static Tuple<TAttribute, TAttribute, bool> EnsureBothHave<TAttribute>(MethodInfo interfaceMethod, MethodInfo classMethod, StringBuilder message)
where TAttribute : Attribute
{
var fromInterface = interfaceMethod.GetAttributes<TAttribute>().SingleOrDefault();
var fromClass = classMethod.GetAttributes<TAttribute>().SingleOrDefault();
var bothHaveTheAttribute = true;
if (fromInterface != null)
{
if (fromClass == null)
{
message.AppendLine(string.Format("Method {0} has {1} on the interface, but not on the class.", interfaceMethod, typeof(TAttribute)));
bothHaveTheAttribute = false;
}
}
else
{
if (fromClass != null)
{
message.AppendLine(string.Format("Method {0} has {1} on the class, but not on the interface.", interfaceMethod, typeof(TAttribute)));
}
bothHaveTheAttribute = false;
}
return Tuple.Create(fromInterface, fromClass, bothHaveTheAttribute);
}
}
All they do is ensure that whenever I obsolete a method on the container, I do that consistently between the interface and the class that implements it (setting the same warning message, and the same warning/error flag state). It also validates that I hide the obsolete method from Intellisense for people who have the option enabled in their Visual Studio.
Those are kinds of things, that are important, but they neither cause a compiler error, or compiler warning, nor do they fail at runtime. Those are kinds of things you can validate in a test. Those are small things that make a big difference, and having a comprehensive battery of tests for conventions in your application, can greatly improve confidence and morale of the team.