One of the implicit key characterstics which define the readability of any Unit Test cases is its ability to be grouped depending on multiple factors.
NUnit uses CategoryAttribute
, while MSTest uses the TestCategoryAttribute
for grouping tests. With xUnit, you could make use the TraitAttribute
to achieve this. However, this is not short of problems of its own.
The most inconvenient part of using Traits are that they are basically a name-value pair of strings. This makes it vurnerable to typos or a headache when decide to change the name. Luckily, xUnit provides us an easy to use extensibility point.
ITraitAttribute and ITraitDiscoverer
You can create your own Custom Traits which could be used to decorate the test cases. For the sake of example, let us create two attributes – FeatureAttribute
and BugAttribute
which would be used to Categorize Tests cases for Features and Bugs.
[TraitDiscoverer(FeatureDiscoverer.TypeName,TraitDiscovererBase.AssemblyName)]
public class FeatureAttribute:Attribute, ITraitAttribute
{
public string Id { get; set; }
public FeatureAttribute(string id) => Id = id;
public FeatureAttribute() { }
}
The attribute implements the ITraitAttribute
interface and has a property to indicate the Id
of the Feature. What is more interesting is the TraitDiscoverer
attribute. It isn’t sufficient that we have the attributes, but it also needs to be discovered by the Test Explorer. This is where the TraitDiscoverer comes into place.
The attribute accepts two parameters, the fully qualified name of the Discoverer associated with the FeatureAttribute
and the assembly which defines it. The Dicoverer for Feature is defined as
public class FeatureDiscoverer : TraitDiscovererBase,ITraitDiscoverer
{
public const string TypeName = TraitDiscovererBase.AssemblyName + ".Helpers.CustomTraits.FeatureDiscoverer";
protected override string CategoryName => "Feature";
public override IEnumerable<KeyValuePair<string, string>> GetTraits(IAttributeInfo traitAttribute)
{
yield return GetCategory();
var id = traitAttribute.GetNamedArgument<string>("Id");
if (!string.IsNullOrEmpty(id))
{
yield return new KeyValuePair<string, string>(TypeName, id);
}
}
}
public class TraitDiscovererBase : ITraitDiscoverer
{
public const string AssemblyName = "Nt.Infrastructure.Tests";
protected const string Category = nameof(Category);
protected virtual string CategoryName => nameof(CategoryName);
protected KeyValuePair<string,string> GetCategory()
{
return new KeyValuePair<string, string>(Category, CategoryName);
}
public virtual IEnumerable<KeyValuePair<string, string>> GetTraits(IAttributeInfo traitAttribute)
{
return Enumerable.Empty<KeyValuePair<string,string>>();
}
}
The Discoverer needs to be implement the ITraitDiscoverer
which has a single method, GetTraits
and returns a collection of KeyValuePairs
. That’s all you need. Now you could decorate your Test Cases as the following
[Theory]
[MemberData(nameof(CreateMovieTest_ResponseStatus_200_TestData))]
[Feature("1523")]
public async Task CreateMovieTest_ResponseStatus_200(CreateMovieRequest request, CreateMovieResponse expectedResult)
{
// Test Case
}
The above is whole lot cleaner than the following
[Trait("Category","Feature")]
[Trait("Feature","1523")]