Skip to content

Instantly share code, notes, and snippets.

@cr7pt0gr4ph7
Last active June 25, 2020 20:58
Show Gist options
  • Save cr7pt0gr4ph7/5065888 to your computer and use it in GitHub Desktop.
Save cr7pt0gr4ph7/5065888 to your computer and use it in GitHub Desktop.
A small demo program in C#, demonstrating the dynamic generation of Xunit tests for each filename in a search path, where the filename matches a pattern. See the included Readme.md file.

A small demo program in C#, demonstrating the dynamic generation of Xunit tests for each filename in a search path, where the filename matches a pattern. This Gist requires xUnit.net to compile.

After reading in the EnumerateFilesFixtureAttribute on the type Tests.AutoPopulatedTest, the main program enumerates all files in the path specified by the attribute that also match the searchPattern (e.g. "*.json").

It then uses System.Reflection.Emit to emit a test method for each of the files found. Each generated test method is also decorated with a [Xunit.FactAttribute] to make the Xunit test runner recognize it.

By modifying the value of the generated DisplayName property, one can change how the generated tests are displayed in the Xunit GUI runner, for example.

== Further information: ==

using System;
namespace XunitAutoTest
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class EnumerateFilesFixtureAttribute : Attribute
{
readonly string path;
readonly string searchPattern;
/// <summary>
/// Creates a new instance of the <see cref="EnumerateFilesFixtureAttribute"/> class.
/// </summary>
/// <param name="path">The directory to search.</param>
/// <param name="searchPattern">The search string to match against the names of files in <paramref name="path"/>.</param>
/// <seealso cref="System.IO.Directory.EnumerateFiles"/>
public EnumerateFilesFixtureAttribute(string path, string searchPattern)
{
this.path = path;
this.searchPattern = searchPattern;
}
public string Path
{
get { return this.path; }
}
public string SearchPattern
{
get { return this.searchPattern; }
}
}
}
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace XunitAutoTest
{
public class Program
{
static string GetTestName(string filePath)
{
var fileName = Path.GetFileNameWithoutExtension(filePath);
if (fileName.Length == 0)
fileName = "_";
else
fileName = string.Concat(fileName.Select((char c) => {
if (char.IsLetterOrDigit(c))
return c;
else
return '_';
}));
if (char.IsDigit(fileName.First()))
fileName = "_" + fileName;
return fileName;
}
static CustomAttributeBuilder BuildFactAttribute(string displayName)
{
var factType = typeof(Xunit.FactAttribute);
var factCtor = typeof(Xunit.FactAttribute).GetConstructor(Type.EmptyTypes);
var attrBuilder = new CustomAttributeBuilder(
factCtor,
new object[] { },
new[] { factType.GetProperty("DisplayName") },
new[] { displayName });
return attrBuilder;
}
static void Main(string[] args)
{
Console.WriteLine("Preparing generation of the dynamic assembly...");
// Create a dynamic assembly to save the generated types to:
AssemblyName aName = new AssemblyName("XunitAutoTest.Generated.Dynamic");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(aName.Name, aName.Name + ".dll");
// The base class of the generated class.
var baseType = typeof(Tests.AutoPopulatedTest);
var baseMeth = baseType.GetMethod("RunTest", new[] { typeof(string) });
// The TypeBilder for our generated test class
var typeBuilder = moduleBuilder.DefineType(
"Json.GeneratedTests.TestDataTests", // * The name of the generated class, including the namespace *
TypeAttributes.Public | TypeAttributes.Class,
baseType
);
// For each [EnumerateFiles(path, searchPattern)] attribute:
foreach (var attr in baseType.GetCustomAttributes<EnumerateFilesFixtureAttribute>())
{
// Enumerate the corresponding file(-names):
foreach (var fileName in Directory.EnumerateFiles(attr.Path, attr.SearchPattern))
{
// And generate a test method for each of them:
var testBuilder = typeBuilder.DefineMethod(GetTestName(fileName), MethodAttributes.Public);
var ilGenerator = testBuilder.GetILGenerator();
// Our "test method" just calls AutoPopulatedTest.RunTest("<value of fileName>").
// ex.:
// AutoPopulatedTest.RunTest("C:\(...)\test-data\test_one.json");
// The value of fileName is stored as a literal value in the generated assembly file.
ilGenerator.Emit(OpCodes.Ldarg_0); // Load the "this" reference
ilGenerator.Emit(OpCodes.Ldstr, fileName); // and the filename string on the stack,
ilGenerator.Emit(OpCodes.Callvirt, baseMeth); // and pass them to AutoPopulatedTest.RunTest(string fileName).
ilGenerator.Emit(OpCodes.Ret);
// End of test method implementation.
// Add a [Fact] attribute to the method.
// 'displayName' is the string which will be shown in the Xunit GUI Runner.
var displayName = "JSON data test: " + fileName;
var factAttr = BuildFactAttribute(displayName);
testBuilder.SetCustomAttribute(factAttr);
}
}
// Finally, create our dynamically generated type:
typeBuilder.CreateType();
Console.WriteLine("Generation finished.");
Console.WriteLine("Preparing writing the assembly to the disk.");
assemblyBuilder.Save(aName.Name + ".dll");
Console.WriteLine("The generated module has been written to disk successfully.");
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}
FAIL { "name" : "Test One" }
{ "name" : "Test Three" }
{ "name" : "Test Two" }
using System;
using System.IO;
using Xunit;
namespace XunitAutoTest.Tests
{
// [EnumerateFilesFixture("test-data", "*.json")]
[EnumerateFilesFixture(".", "*.json")] // GitHub gists don't support subdirectories, so the directory structure has been flattened.
public abstract class AutoPopulatedTest
{
public void RunTest(string fileName)
{
Console.WriteLine(">> {0}:", fileName);
Assert.NotNull(fileName);
Assert.NotEmpty(fileName);
var fileContents = File.ReadAllText(fileName);
Console.WriteLine("fileContents");
Console.WriteLine();
Console.WriteLine();
Assert.False(fileContents.StartsWith("FAIL"));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment