Testing conventions

I already blogged about the topic of val­i­dat­ing con­ven­tions in the past (here and here). Doing this has been a fan­tas­tic way of keep­ing con­sis­tency across code­bases I’ve worked on, and sev­eral of my col­leagues at Read­ify adopted this approach with great success.

Recently I found myself using this approach even more often and in sce­nar­ios I didn’t think about ini­tially. 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 when­ever I obso­lete a method on the con­tainer, I do that con­sis­tently between the inter­face and the class that imple­ments it (set­ting the same warn­ing mes­sage, and the same warning/error flag state). It also val­i­dates that I hide the obso­lete method from Intel­lisense for peo­ple who have the option enabled in their Visual Studio.

Those are kinds of things, that are impor­tant, but they nei­ther cause a com­piler error, or com­piler warn­ing, nor do they fail at run­time. Those are kinds of things you can val­i­date in a test. Those are small things that make a big dif­fer­ence, and hav­ing a com­pre­hen­sive bat­tery of tests for con­ven­tions in your appli­ca­tion, can greatly improve con­fi­dence and morale of the team.