54

When you use [Theory] together with [InlineData] it will create a test for each item of inline data that is provided. However, if you use [MemberData] it will just show up as one test.

Is there a way to make [MemberData] tests show up as multiple tests?

user247702
  • 23,641
  • 15
  • 110
  • 157
NPadrutt
  • 3,619
  • 5
  • 24
  • 60
  • IIRC there is a github issue covering this and/or it has been covered there – Ruben Bartelink Jun 01 '15 at 19:48
  • Du you remember which issue it was? I couldn't find an issue who describes this. – NPadrutt Jun 02 '15 at 08:32
  • Sorry couldn't find anything that matches clearly. Perhaps reading [this one](https://github.com/xunit/xunit/issues/7) might help you to verbalize what you're looking for as an issue on GitHub (for instance discovery, pre-enumerating theories, test method vs test case). I'd also have a look in the docs in order to determine what is the intended behavior for v2 and for v1 backcompat (i.e. pre-enumerating theories is a double edged sword if one needs to spin up/down resources per test case) – Ruben Bartelink Jun 02 '15 at 11:22
  • Alright, thanks. I submitted a new issue. We'll see what they think :) – NPadrutt Jun 02 '15 at 14:52

5 Answers5

44

I spent a lot of time trying to figure this one out in my project. This related Github discussion from @NPadrutt himself helped a lot, but it was still confusing.

The tl;dr is this: [MemberInfo] will report a single group test unless the provided objects for each test can be completely serialized and deserialized by implementing IXunitSerializable.


Background

My own test setup was something like:

public static IEnumerable<object[]> GetClients()
{
    yield return new object[] { new Impl.Client("clientType1") };
    yield return new object[] { new Impl.Client("clientType2") };
}

[Theory]
[MemberData(nameof(GetClients))]
public void ClientTheory(Impl.Client testClient)
{
    // ... test here
}

The test ran twice, once for each object from [MemberData], as expected. As @NPadrutt experienced, only one item showed up in the Test Explorer, instead of two. This is because the provided object Impl.Client was not serializable by either interface xUnit supports (more on this later).

In my case, I didn't want to bleed test concerns into my main code. I thought I could write a thin proxy around my real class that would fool the xUnit runner into thinking it could serialize it, but after fighting with it for longer than I'd care to admit, I realized the part I wasn't understanding was:

The objects aren't just serialized during discovery to count permutations; each object is also deserialized at test run time as the test starts.

So any object you provide with [MemberData] must support a full round-trip (de-)serialization. This seems obvious to me now, but I couldn't find any documentation on it while I was trying to figure it out.


Solution

  • Make sure every object (and any non-primitive it may contain) can be fully serialized and deserialized. Implementing xUnit's IXunitSerializable tells xUnit that it's a serializable object.

  • If, as in my case, you don't want to add attributes to the main code, one solution is to make a thin serializable builder class for testing that can represent everything needed to recreate the actual class. Here's the above code, after I got it to work:

TestClientBuilder

public class TestClientBuilder : IXunitSerializable
{
    private string type;

    // required for deserializer
    public TestClientBuilder()
    {
    }

    public TestClientBuilder(string type)
    {
        this.type = type;
    }

    public Impl.Client Build()
    {
        return new Impl.Client(type);
    }

    public void Deserialize(IXunitSerializationInfo info)
    {
        type = info.GetValue<string>("type");
    }

    public void Serialize(IXunitSerializationInfo info)
    {
        info.AddValue("type", type, typeof(string));
    }

    public override string ToString()
    {
        return $"Type = {type}";
    }
}

Test

public static IEnumerable<object[]> GetClients()
{
    yield return new object[] { new TestClientBuilder("clientType1") };
    yield return new object[] { new TestClientBuilder("clientType2") };
}

[Theory]
[MemberData(nameof(GetClients))]
private void ClientTheory(TestClientBuilder clientBuilder)
{
    var client = clientBuilder.Build();
    // ... test here
}

It's mildly annoying that I don't get the target object injected anymore, but it's just one extra line of code to invoke my builder. And, my tests pass (and show up twice!), so I'm not complaining.

Mardoxx
  • 4,372
  • 7
  • 41
  • 67
Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
  • Indeed mildly annoying. Any update on better ways of doing this? – Strake Feb 21 '17 at 20:45
  • @Strake Not from my side. This is the solution I've been using and it works, other than being ugly. – Nate Barbettini Feb 21 '17 at 22:22
  • 2
    At least in vs2019, this causes the test explorer to infinitely load, and locks down build files so a rebuild isn't possible till VS is restarted. Test output doesn't show anything in particular. Not a problem in vs2017. Wonder why... – Douglas Gaskell Oct 02 '19 at 22:14
  • I'm trying to use this code with Selenium for automated testing. I can't figure out how to use a property of type 'By' as a parameter instead of a string. Any ideas? public By select_project_search_bar { get; set; } = By.XPath("//*[@id=\"ctl00_body\"]/span/span/span[1]/input"); – agleno Mar 18 '21 at 16:46
  • @agleno Sounds specific enough that it could be its own question. Just from your comment I can't quite tell what is going on. – Nate Barbettini Mar 18 '21 at 23:02
  • @NateBarbettini I was chancing my arm! I'm using Selenium for automated testing and I have a class that stores the Xpaths of elements on a webpage. I'm trying to use Xunit to parametrize some tests, e.g I'm testing some navigation so It's the same test for every page, i'm just passing the xpath and the expected page title and asserting if it's correct or not. The issue with Xunit/Visual Studio is, it's not discovering each test individually. The code samples here work but only for strings. I need to somehow trick Xunit into discovering my parameterized tests. I've asked elsewhere........ – agleno Mar 19 '21 at 09:00
  • @NateBarbettini I can't seem to serialize the data type "By" used in selenium in the same way you guys have used a string here. I have asked on SO overflow before and even contact the Xunit creators with no luck! This page is the closest I've ever seen to doing exactly what I need it to do – agleno Mar 19 '21 at 09:02
  • e.g. https://stackoverflow.com/questions/66138629/xunit-parameterized-tests-class-properties-c-sharp/66178687?noredirect=1#comment117064725_66178687 – agleno Mar 19 '21 at 09:06
30

MemberData can work with properties or methods which return IEnumerable of object[]. You will see a separate test result for each yield in this scenario:

public class Tests
{ 
    [Theory]
    [MemberData("TestCases", MemberType = typeof(TestDataProvider))]
    public void IsLargerTest(string testName, int a, int b)
    {
        Assert.True(b>a);
    }
}

public class TestDataProvider
{
    public static IEnumerable<object[]> TestCases()
    {
        yield return new object[] {"case1", 1, 2};
        yield return new object[] {"case2", 2, 3};
        yield return new object[] {"case3", 3, 4};
    }
}

However, as soon as you will need to pass complex custom objects no matter how many test cases you will have the test output window will show just one test. This is not ideal behaviour and indeed very inconvenient while debugging which test case is failing. The workaround is to create your own wrapper which will derive from IXunitSerializable.

public class MemberDataSerializer<T> : IXunitSerializable
    {
        public T Object { get; private set; }

        public MemberDataSerializer()
        {
        }

        public MemberDataSerializer(T objectToSerialize)
        {
            Object = objectToSerialize;
        }

        public void Deserialize(IXunitSerializationInfo info)
        {
            Object = JsonConvert.DeserializeObject<T>(info.GetValue<string>("objValue"));
        }

        public void Serialize(IXunitSerializationInfo info)
        {
            var json = JsonConvert.SerializeObject(Object);
            info.AddValue("objValue", json);
        }
    }

Now you can have your custom objects as parameters to Xunit Theories and still see/debug them as independent results in the test runner window:

public class UnitTest1
{
    [Theory]
    [MemberData("TestData", MemberType = typeof(TestDataProvider))]
    public void Test1(string testName, MemberDataSerializer<TestData> testCase)
    {
        Assert.Equal(1, testCase.Object.IntProp);
    }
}

public class TestDataProvider
{
    public static IEnumerable<object[]> TestData()
    {
        yield return new object[] { "test1", new MemberDataSerializer<TestData>(new TestData { IntProp = 1, StringProp = "hello" }) };
        yield return new object[] { "test2", new MemberDataSerializer<TestData>(new TestData { IntProp = 2, StringProp = "Myro" }) };      
    }
}

public class TestData
{
    public int IntProp { get; set; }
    public string StringProp { get; set; }
}

Hope this helps.

user1807319
  • 301
  • 3
  • 2
  • 2
    The serializer works great. I've slightly modified it to also have a `Description` property and use it in `ToString()`. That way, you can have (for example) `"test1"` and `"test2"` show up in the Test Explorer. – user247702 Sep 15 '17 at 08:13
  • 1
    I'm a bit upset. I've created an issue for this in the xUnit repo and this is what I got: https://github.com/xunit/xunit/issues/1679#issuecomment-375929120. – SuperJMN Mar 26 '18 at 17:31
  • I ended up with the same implementation result as you, but while writing my custom JSON attribute. It`s a pity that we cannot have an accessor for the final data in our IXunitSerializable. – Nordes Aug 27 '18 at 12:07
  • I'm trying to use this code with Selenium for automated testing. I can't figure out how to use a property of type 'By' as a parameter instead of a string. Any ideas? public By select_project_search_bar { get; set; } = By.XPath("//*[@id=\"ctl00_body\"]/span/span/span[1]/input"); – agleno Mar 18 '21 at 16:46
6

In my recent project I experienced the very same problem and after some research the solution which I came up with is as follows:

Implement your custom MyTheoryAttribute extending FactAttribute along with MyTheoryDiscoverer implementing IXunitTestCaseDiscoverer and several custom MyTestCases extending TestMethodTestCase and implementing IXunitTestCase to your liking. Your custom test cases should be recognized by MyTheoryDiscoverer and used to encapsulate your enumerated theory test cases in form visible to Xunit framework even if values passed are not serialized natively by Xunit and do not implement IXunitSerializable.

What is most important there is no need to change your precious code under test!

It's a bit of work to do but since it was done already by me and is available under MIT license feel free to use it. It is part of DjvuNet project which is hosted on GitHub.

Direct link to the relevant folder with Xunit support code is below:

DjvuNet test support code

To use it either create separate assembly with this files or include them directly into your test project.

Usage is exactly the same as with Xunit TheoryAttribute and both ClassDataAttribute and MemberDataAttribute are supported i.e.:

[DjvuTheory]
[ClassData(typeof(DjvuJsonDataSource))]
public void InfoChunk_Theory(DjvuJsonDocument doc, int index)
{
    // Test code goes here
}


[DjvuTheory]
[MemberData(nameof(BG44TestData))]
public void ProgressiveDecodeBackground_Theory(BG44DataJson data, long length)
{
    // Test code goes here
}

Credit goes as well to another developer but unfortunately I cannot find his repo on github

Jacek Blaszczynski
  • 3,183
  • 14
  • 25
  • I've tried it and it Works! It's a shame this code isn't included in the oficial xUnit. Big thanks! Do you know if this piece of code is uploaded somewhere as a Nuget Package? – SuperJMN Mar 24 '18 at 00:10
  • Not Nuget but source yes, the code is part of DjvuNet project (MIT License). https://github.com/DjvuNet/DjvuNet/tree/master/DjvuNet.Shared.Tests/xunit – Jacek Blaszczynski Mar 25 '18 at 10:35
  • 1
    I'm bit upset. I created an issue into the xUnit repo and this is what I got: https://github.com/xunit/xunit/issues/1679#issuecomment-375929120 – SuperJMN Mar 26 '18 at 17:32
  • I'm trying to use this code with Selenium for automated testing. I can't figure out how to use a property of type 'By' as a parameter instead of a string. Would DJVU work with that? public By select_project_search_bar { get; set; } = By.XPath("//*[@id=\"ctl00_body\"]/span/span/span[1]/input"); – agleno Mar 18 '21 at 16:47
2

For now, ReSharper can show all MemberData tests with custom parameters when your custom classes overrides ToString().

For example :

public static TheoryData<Permission, Permission, Permission> GetAddRuleData()
{
    var data = new TheoryData<Permission, Permission, Permission>
    {
        {
            new Permission("book", new[] {"read"}, null),
            new Permission("book", new[] {"delete"}, new[] {"2333"}),
            new Permission("book", new[] {"delete", "read"}, new[] {"*", "2333"})
        },
        {
            new Permission("book", new[] {"read"}, null),
            new Permission("music", new[] {"read"}, new[] {"2333"}), new Permission
            {
                Resources = new Dictionary<string, ResourceRule>
                {
                    ["book"] = new ResourceRule("book", new[] {"read"}, null),
                    ["music"] = new ResourceRule("music", new[] {"read"}, new[] {"2333"}),
                }
            }
        }
    };
    return data;
}

Permission overrides ToString(), then in ReSharper Test Session Explorer:

xunitR#

Tao Zhu
  • 749
  • 6
  • 12
  • yes, I saw that too. Except that it`s not everyone who uses R#. At least, when it runs in console it gives the proper results (number of tests and all). – Nordes Aug 27 '18 at 12:04
1

A simple alternative is that if you are using .net core project, instead of using vstest explorer you can run your tests in command line using "dotnet test"

The results are:

  • get the total amount of tests
  • amount that passed
  • amount that failed

For member data tests that fail, you will get the associated parameter values for each of the failed member data test

Hitman Dev
  • 11
  • 1