5

I am trying to write a generic theory in xunit.net that uses collections via the MemberDataAttribute. Please have a look at the following code:

[Theory]
[MemberData("TestData")]
public void AddRangeTest<T>(List<T> items)
{
    var sut = new MyCustomList<T>();

    sut.AddRange(items);

    Assert.Equal(items, sut);
}

public static readonly IEnumerable<object[]> TestData = new[]
    {
        new object[] { new List<int> { 1, 2, 3 } },
        new object[] { new List<string> { "Foo", "Bar" } }
    };

When I try to execute this theory, the following exceptions are thrown: "System.ArgumentException: Object of type 'List'1[System.String] / List'1[System.Int32]' cannot be converted to type 'List'[System.Object]'." (I shortened and merged the text of both exceptions).

I get that this could maybe be related to the parameter type as it is not directly a generic type but a type that uses a nested generic. Thus I transformed the test in the following way:

[Theory]
[MemberData("TestData")]
public void AddRangeTest2<T, TCollection>(TCollection items)
    where TCollection : IEnumerable<T>
{
    var sut = new MyCustomList<T>();

    sut.AddRange(items);

    Assert.Equal(items, sut);
}

In this case, I introduced a generic called TCollection that is contrained to be an IEnumerable<T> instance. When I execute this test, the run with List<string> works, but the run with List<int> produces the following exception: "System.ArgumentException: GenericArguments[1], 'List'1[System.Int32]', on 'AddRangeTest2' violates the constraint of type 'TCollection'." (Again, I shortened the exception text to the relevant points).

My actual question is: why is it possible to use List<string> but not List<int> in a generic theory? Both of these types satisfy the constraint on the TCollection generic in my opinion.

If I change List<int> to List<object>, then everything works - thus I assume that it has something to do with Value Types / Reference Types and Co- / Contravariance.

I use xunit 2.0.0-rc2 although I also observed this behavior with rc1 - thus I think it is version-independent (and possibly not even a problem of xunit). If you want to have a look at the full source code, you can download it from my dropbox (I created it with VS 2013). Please consider this code to be an MCVE.

Thank you so much in advance for your help!

Edit after this question was closed: my question is not answered by Cannot convert from List<DerivedClass> to List<BaseClass> because I do not use inheritance-based casts here at all - instead I believe that xunit.net uses generics that are resolved via reflection.

Community
  • 1
  • 1
feO2x
  • 5,358
  • 2
  • 37
  • 46
  • @Yuval Itzchakov: can you please re-open my question because I do not try to cast `List` to `List`? I want to do `List` to `TCollection where TCollection : IEnumerable`. – feO2x Feb 18 '15 at 11:04
  • 1
    I faced similiar problem today, and I just remembered that you had similar problem. Anyways, this is how I solved it: https://github.com/ChrisEelmaa/AlgorithmExamples/tree/master/Tests/Sorting I didn't find better way & as for now, it will do. – Erti-Chris Eelmaa May 16 '15 at 22:50

1 Answers1

2

Whatever you do, it still boils down to the fact that there is going on IEnumerable<int> to IEnumerable<object> conversion, behind the scenes.

You seem to be under assumption that the generic parameters are filled based on the concrete input given.

That is not the case. The generic type parameters are deduced from the variable that is referenced in MemberData, not the actual content it is holding, thus, your test method will be always called with the next signature:

 AddRangeTest2<object, object[]>(object[] items)

The above signature supports implicitly covariance for reference types (meaning: you can pass string[] to this method, but not int[]).

Covariance for value types is not supported.


This said, it would be cool if xUnit is clever enough to specify the correct generic type arguments, based on the actual input. I don't have experience with xUnit myself, perhaps there is already a way? If not, feature request in codeplex, or make it yourself + pull request ;)
Community
  • 1
  • 1
Erti-Chris Eelmaa
  • 25,338
  • 6
  • 61
  • 78
  • I'll have a look at the xunit implementation and check if it is feasible. Thank you so much for your answer - it improved my understanding about the topic. – feO2x Feb 18 '15 at 12:57
  • After having a night to sleep and think about your answer, I came to the conclusion that it is unfortunately not correct. In xunit.net, you have to declare an `IEnumerable` to use the `MemberDataAttribute` correctly. The outer IEnumerable contains all the arrays that will be used for the runs of the test method (its count determines how often the Theory is run). The inner `object[]`arrays contain the arguments for each run - in my case only one element (the collection). Thus items of the inner array are matched against the arguments of the test method, not the array itself. – feO2x Feb 19 '15 at 07:18
  • Thus your proposal that `public static readonly IEnumerable TestData` will always be resolved to `AddRangeTest2(object[] items)` is wrong. – feO2x Feb 19 '15 at 07:21
  • @feO2x: Interesting. Have you verified that? `Debug.WriteLine(typeof(T).Name)` – Erti-Chris Eelmaa Feb 19 '15 at 10:43
  • Yes, I did. `T` becomes `object` and `TCollection` becomes `List` (`AddRangeTest2` is not called with `List` because of the exception I mentioned in the question - you actually have to comment the line `new object[] { new List { 1, 2, 3 } },` so that xunit.net starts in debug mode). – feO2x Feb 20 '15 at 12:18
  • @feO2x: If I understand you correctly, T will be always object then? If yes, then you cant pass ListInt to TCollection, as there is constraint that TCollection has to be IEnumerable of objects. – Erti-Chris Eelmaa Feb 26 '15 at 22:07
  • In my opinion, it has something to do with the xunit.net algorithm that resolves the generics of a theory - propably it will just use `System.Object` for `T` as it is not used any further in this method (just for the generic contstraint). However, `TCollection` is resolved to the correct type `List` (not `List`) - but if this is the case, I wonder why this does not work for `List` in the same way. Unfortunately, I only had a short look to the xunit.net implementation and haven't really identified the parts in code that I would have to change. – feO2x Feb 27 '15 at 09:20
  • 1
    @feO2x, if xUnit is trying to initiate new generic method at runtime, CLR will refuse because you cant create a method that has TCollection as ListInt, it doesnt accept it, as the generic constraint is not fulfilled. ListInt is not IEnumerable of objects, there is no covariance conversion. Also, if you read the error, it is what its saying: ListInt is violating the constraint. – Erti-Chris Eelmaa Feb 27 '15 at 10:18