We have some unit tests that we call 'quality tests'. They basically reflect over the NUnit tests in a particular DLL to detect if a constructor is used for set-up rather than a method decorated with [TestFixtureSetUp].
Since there's no way to ask, via reflection, 'is this a default constructor or did someone explicitly add it', we look at the code size.
We're using Mono.Cecil to do this. Here's the [NCrunch] test:
Code:
[Behaviors]
public class ATestAssemblyThatHasAMinimumQualityBarBehaviour
{
It Has_no_test_fixtures_where_test_fixture_setup_is_done_in_the_constructor = () =>
{
var errors = new List<string>();
IEnumerable<TypeDefinition> typesWithAttributes = reflector.AllTypes.Where(t => t.HasCustomAttributes);
foreach (TypeDefinition eachTest in typesWithAttributes.Where(t => !t.IsNested))
{
try
{
Mono.Collections.Generic.Collection<CustomAttribute> customAttributes = eachTest.CustomAttributes;
IEnumerable<CustomAttribute> testFixtureAttributes =
customAttributes.Where(a => a.AttributeType.Name == @"TestFixtureAttribute");
if (testFixtureAttributes.Any())
{
bool isParameterised = customAttributes.First().ConstructorArguments.Any();
if (isParameterised)
{
continue;
}
MethodDefinition[] constructors =
eachTest.GetConstructors().Where(c => !c.IsStatic).ToArray();
if (!constructors.Any())
{
continue;
}
if (constructors.Length > 1)
{
errors.Add(@"Test '{0}' has multiple constructors. Tests don't normally need constructors. Are you sure you need them? Are you sure you don't need a TestFixtureSetup method instead?"
.FormatWith(eachTest));
}
bool hasSetup = eachTest.Methods.Any(
m => m.CustomAttributes.Any( a => a.AttributeType.Name == @"TestFixtureSetUpAttribute"));
MethodBody body = constructors[0].Body;
if (hasSetup)
{
continue;
}
Debug.WriteLine($"{eachTest.FullName} has a code size of {body.CodeSize} bytes");
// There's no actual way of determining a 'generated' default constructor via metadata, so
// we guess based on code size (~23 is an empty constructor).
// The size also differes greatly depending on what is running this test; for one test, NCrunch reports 203 bytes, whereas R# reports 23 bytes.
if (body.CodeSize > 23)
{
errors.Add(@"Test '{0}' has a constructor with a body but doesn't have a TestFixtureSetup method. Tests don't normally need a constructor. Are you sure you need it? Are you sure you don't need a TestFixtureSetup method instead?"
.FormatWith(eachTest));
}
}
}
catch (AssemblyResolutionException)
{
}
}
if (errors.Any())
{
string messages = string.Join(Environment.NewLine, errors);
Debug.Write(messages);
if (!string.IsNullOrEmpty(messages))
{
Assert.Fail(messages);
}
}
};
It Has_no_classes_where_the_attribute_does_not_match_the_type_under_test = () =>
{
var errors = new List<string>();
IEnumerable<TypeDefinition> typesWithAttributes = reflector.AllTypes.Where(t => t.HasCustomAttributes);
foreach (TypeDefinition eachTest in typesWithAttributes.Where(t => !t.IsNested))
{
Mono.Collections.Generic.Collection<CustomAttribute> customAttributes = eachTest.CustomAttributes;
foreach (
CustomAttribute eachSubjectAttribute in
customAttributes.Where(
a => a.AttributeType.Name == @"SubjectAttribute" && a.HasConstructorArguments && a.ConstructorArguments.First().GetType()!=typeof(string)))
{
CustomAttributeArgument customAttributeArgument = eachSubjectAttribute.ConstructorArguments[0];
TypeReference typeReferenceOfSut = customAttributeArgument.Type;
//ignore nested Builder classes
if (customAttributeArgument.Value.ToString().EndsWith("/Builder", StringComparison.InvariantCulture))
{
continue;
}
if (typeReferenceOfSut.FullName == @"System.Type")
{
string nameOfSut = ((TypeReference)customAttributeArgument.Value).Name;
if (nameOfSut.Contains('`'))
{
nameOfSut = nameOfSut.Substring(0, nameOfSut.IndexOf('`'));
}
string expectedNameOfTestType = nameOfSut + @"Specs";
if (eachTest.Name != expectedNameOfTestType)
{
errors.Add(@"SUT was {0} but test name was {1} (should be {2})".FormatWith(nameOfSut,
eachTest.Name,
expectedNameOfTestType));
}
}
}
}
if (errors.Any())
{
string messages = string.Join(Environment.NewLine, errors);
Debug.Write(messages);
if (!string.IsNullOrEmpty(messages))
{
Assert.Fail(messages);
}
}
};
protected static Reflector reflector;
}
When run in the ReSharper test runner, it reports that the size of a test's constructor is 23 bytes, but when running under NCrunch, it reports it's 203 bytes.
Does NCrunch do anything with the IL?