30

In xUnit I can have a Theory test that uses generics in this form:

[Theory]
[MemberData(SomeScenario)]
public void TestMethod<T>(T myType)
{
    Assert.Equal(typeof(double), typeof(T));
}

public static IEnumerable<object[]> SomeScenario()
{
    yield return new object[] { 1.23D };
}

Which will give me the generic T parameter as double. Is it possible to use MemberData to specify the generic type parameter for a test with a signature like:

[Theory]
[MemberData(SomeTypeScenario)]
public void TestMethod<T>()
{
    Assert.Equal(typeof(double), typeof(T));
}

If it is not possible with MemberData or any other provided attribute (which I'm suspecting that it isn't), is it possible to create an attribute for Xunit that can achieve this? Maybe something along the lines of specifying Types in the Scenarios method and using reflection in a similar manner to Jon Skeet's answer here: Generics in C#, using type of a variable as parameter

Community
  • 1
  • 1
Ayb4btu
  • 3,208
  • 5
  • 30
  • 42

1 Answers1

27

You can simply include Type as an input parameter instead. E.g.:

[Theory]
[MemberData(SomeTypeScenario)]
public void TestMethod(Type type) {
  Assert.Equal(typeof(double), type);
}

public static IEnumerable<object[]> SomeScenario() {
  yield return new object[] { typeof(double) };
}

There is no need to go with generics on xunit.

Edit (if you really need generics)

1) You need to subclass ITestMethod to persist generic method info, it also has to implement IXunitSerializable

// assuming namespace Contosco
public class GenericTestMethod : MarshalByRefObject, ITestMethod, IXunitSerializable
{
    public IMethodInfo Method { get; set; }
    public ITestClass TestClass { get; set; }
    public ITypeInfo GenericArgument { get; set; }

    /// <summary />
    [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
    public GenericTestMethod()
    {
    }

    public GenericTestMethod(ITestClass @class, IMethodInfo method, ITypeInfo genericArgument)
    {
        this.Method = method;
        this.TestClass = @class;
        this.GenericArgument = genericArgument;
    }

    public void Serialize(IXunitSerializationInfo info)
    {
        info.AddValue("MethodName", (object) this.Method.Name, (Type) null);
        info.AddValue("TestClass", (object) this.TestClass, (Type) null);
        info.AddValue("GenericArgumentAssemblyName", GenericArgument.Assembly.Name);
        info.AddValue("GenericArgumentTypeName", GenericArgument.Name);
    }

    public static Type GetType(string assemblyName, string typeName)
    {
#if XUNIT_FRAMEWORK    // This behavior is only for v2, and only done on the remote app domain side
        if (assemblyName.EndsWith(ExecutionHelper.SubstitutionToken, StringComparison.OrdinalIgnoreCase))
            assemblyName = assemblyName.Substring(0, assemblyName.Length - ExecutionHelper.SubstitutionToken.Length + 1) + ExecutionHelper.PlatformSuffix;
#endif

#if NET35 || NET452
        // Support both long name ("assembly, version=x.x.x.x, etc.") and short name ("assembly")
        var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName == assemblyName || a.GetName().Name == assemblyName);
        if (assembly == null)
        {
            try
            {
                assembly = Assembly.Load(assemblyName);
            }
            catch { }
        }
#else
        System.Reflection.Assembly assembly = null;
        try
        {
            // Make sure we only use the short form
            var an = new AssemblyName(assemblyName);
            assembly = System.Reflection.Assembly.Load(new AssemblyName { Name = an.Name, Version = an.Version });

        }
        catch { }
#endif

        if (assembly == null)
            return null;

        return assembly.GetType(typeName);
    }

    public void Deserialize(IXunitSerializationInfo info)
    {
        this.TestClass = info.GetValue<ITestClass>("TestClass");
        string assemblyName = info.GetValue<string>("GenericArgumentAssemblyName");
        string typeName = info.GetValue<string>("GenericArgumentTypeName");
        this.GenericArgument = Reflector.Wrap(GetType(assemblyName, typeName));
        this.Method = this.TestClass.Class.GetMethod(info.GetValue<string>("MethodName"), true).MakeGenericMethod(GenericArgument);
    }
}

2) You need to write your own discoverer for generic methods, it has to be subclass of IXunitTestCaseDiscoverer

// assuming namespace Contosco
public class GenericMethodDiscoverer : IXunitTestCaseDiscoverer
{
    public GenericMethodDiscoverer(IMessageSink diagnosticMessageSink)
    {
        DiagnosticMessageSink = diagnosticMessageSink;
    }

    protected IMessageSink DiagnosticMessageSink { get; }

    public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions,
        ITestMethod testMethod, IAttributeInfo factAttribute)
    {
        var result = new List<IXunitTestCase>();
        var types = factAttribute.GetNamedArgument<Type[]>("Types");
        foreach (var type in types)
        {
            var typeInfo = new ReflectionTypeInfo(type);
            var genericMethodInfo = testMethod.Method.MakeGenericMethod(typeInfo);
            var genericTestMethod = new GenericTestMethod(testMethod.TestClass, genericMethodInfo, typeInfo);

            result.Add(
                new XunitTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(),
                    genericTestMethod));
        }

        return result;
    }
}

3) Finally you can make your attribute for generic methods and hook it to your custom discoverer by XunitTestCaseDiscoverer attribute

// assuming namespace Contosco
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
[XunitTestCaseDiscoverer("Contosco.GenericMethodDiscoverer", "Contosco")]
public sealed class GenericMethodAttribute : FactAttribute
{
    public Type[] Types { get; private set; }

    public GenericMethodAttribute(Type[] types)
    {
        Types = types;
    }
}

Usage:

[GenericMethod(new Type[] { typeof(double), typeof(int) })]
public void TestGeneric<T>()
{
  Assert.Equal(typeof(T), typeof(double));
}
Ondrej Svejdar
  • 21,349
  • 5
  • 54
  • 89
  • 3
    The example I gave in the question was a simplistic case. What if I wanted to test a method with generics that only takes in a generic parameter, e.g.: `public int MyMethod(string someInput)`? I could call this method passing in a `Type` using reflection, but this gets messy. – Ayb4btu Jul 19 '17 at 01:07
  • I'm getting an assembly reference error on `Xunit.Sdk.LongLivedMarshalByRefObject`. Am I missing something? – Ayb4btu Jul 20 '17 at 09:41
  • @Ayb4btu - you can substitue for MarshalByRefObject instead – Ondrej Svejdar Jul 20 '17 at 10:07
  • `Xunit.LongLivedMarshalByRefObject` is available, but visual studio Test Explorer can't seem to find the test. Everything has Contosco as the namespace. Could it be an xUnit version or dotnet core (which I'm using) issue? – Ayb4btu Jul 23 '17 at 02:31
  • 2
    I'm getting Message: System.InvalidOperationException : Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true. Stack Trace: RuntimeMethodInfo.ThrowNoInvokeException() RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) MethodBase.Invoke(Object obj, Object[] parameters) – Antao Almada Oct 16 '19 at 17:12
  • Im getting VsTest test-case is missing. Rebuild the project and try again. Rebuild don't help ofc – Paweł Kanarek Jun 04 '20 at 22:44