Lambda expression as inline data in xUnit

南笙酒味 提交于 2019-12-22 18:27:01

问题


I'm pretty new to xUnit and here's what I'd like to achieve:

[Theory]
[InlineData((Config y) => y.Param1)]
[InlineData((Config y) => y.Param2)]
public void HasConfiguration(Func<Config, string> item)
{
    var configuration = serviceProvider.GetService<GenericConfig>();
    var x = item(configuration.Config1); // Config1 is of type Config

    Assert.True(!string.IsNullOrEmpty(x));            
}

Basically, I have a GenericConfig object which contains Config and other kind of configurations, but I need to check that every single parameter is valid. Since they're all string, I wanted to simplify using [InlineData] attribute instead of writing N equals tests.

Unfortunately the error I'm getting is "Cannot convert lambda expression to type 'object[]' because it's not a delegate type", which is pretty much clear.

Do you have any idea on how to overcome this?


回答1:


In addition to the already posted answers. The test cases can be simplified by directly yielding the lambdas.

public class ConfigTestDataProvider
{
    public static IEnumerable<object[]> TestCases
    {
        get
        {
            yield return new object [] { (Func<Config, object>)((x) => x.Param1) };
            yield return new object [] { (Func<Config, object>)((x) => x.Param2) };
        }
    }
}

This test ConfigTestDataProvider can then directly inject the lambdas.

[Theory]
[MemberData(nameof(ConfigTestCase.TestCases), MemberType = typeof(ConfigTestCase))]
public void Test(Func<Config, object> func)
{
    var config = serviceProvider.GetService<GenericConfig>();
    var result = func(config.Config1);

    Assert.True(!string.IsNullOrEmpty(result));
}



回答2:


Actually, I was able to find a solution which is a bit better than the one provided by Iqon (thank you!).

Apparently, the InlineData attribute only supports primitive data types. If you need more complex types, you can use the MemberData attribute to feed the unit test with data from a custom data provider.

Here's how I solved the problem:

public class ConfigTestCase
{
    public static readonly IReadOnlyDictionary<string, Func<Config, string>> testCases = new Dictionary<string, Func<Config, string>>
    {
        { nameof(Config.Param1), (Config x) => x.Param1 },
        { nameof(Config.Param2), (Config x) => x.Param2 }
    }
    .ToImmutableDictionary();

    public static IEnumerable<object[]> TestCases
    {
        get
        {
            var items = new List<object[]>();

            foreach (var item in testCases)
                items.Add(new object[] { item.Key });

            return items;
        }
    }
}

And here's the test method:

[Theory]
[MemberData(nameof(ConfigTestCase.TestCases), MemberType = typeof(ConfigTestCase))]
public void Test(string currentField)
{
    var func = ConfigTestCase.testCases.FirstOrDefault(x => x.Key == currentField).Value;
    var config = serviceProvider.GetService<GenericConfig>();
    var result = func(config.Config1);

    Assert.True(!string.IsNullOrEmpty(result));
}

I could maybe come up with something a bit better or cleaner, but for now it works and the code is not duplicated.




回答3:


I have the problem the same to you, and I found the solution that using TheoryData class and MemberData attribute. Here is the example and I hope the code usefully:

public class FooServiceTest
{
    private IFooService _fooService;
    private Mock<IFooRepository> _fooRepository;

    //dummy data expression
    //first parameter is expression
    //second parameter is expected
    public static TheoryData<Expression<Func<Foo, bool>>, object> dataExpression = new TheoryData<Expression<Func<Foo, bool>>, object>()
    {
        { (p) => p.FooName == "Helios", "Helios" },
        { (p) => p.FooDescription == "Helios" && p.FooId == 1, "Helios" },
        { (p) => p.FooId == 2, "Poseidon" },
    };

    //dummy data source
    public static List<Foo> DataTest = new List<Foo>
    {
        new Foo() { FooId = 1, FooName = "Helios", FooDescription = "Helios Description" },
        new Foo() { FooId = 2, FooName = "Poseidon", FooDescription = "Poseidon Description" },
    };

    //constructor
    public FooServiceTest()
    {
        this._fooRepository = new Mock<IFooRepository>();
        this._fooService = new FooService(this._fooRepository.Object);
    }

    [Theory]
    [MemberData(nameof(dataExpression))]
    public void Find_Test(Expression<Func<Foo, bool>> expression, object expected)
    {
        this._fooRepository.Setup(setup => setup.FindAsync(It.IsAny<Expression<Func<Foo, bool>>>()))
                               .ReturnsAsync(DataTest.Where(expression.Compile()));

        var actual = this._fooService.FindAsync(expression).Result;
        Assert.Equal(expected, actual.FooName);
    }
}



回答4:


Oddly delegates are not objects, but Actions or Funcs are. To do this, you have to cast the lambda to one of these types.

 object o = (Func<Config, string>)((Config y) => y.Param1)

But doing this, your expression is not constant anymore. So this will prevent usage in an Attribute.

There is no way of passing lambdas as attributes.

One possible solution would be to use function calls, instead of attributes. Not as pretty, but could solve your problem without duplicate code:

private void HasConfiguration(Func<Config, string> item)
{
    var configuration = serviceProvider.GetService<GenericConfig>();
    var x = item(configuration.Config1); // Config1 is of type Config

    Assert.True(!string.IsNullOrEmpty(x));            
}

[Theory]
public Test1()
{
    HasConfiguration((Config y) => y.Param1);
}    

[Theory]
public Test2()
{
    HasConfiguration((Config y) => y.Param2);
}


来源:https://stackoverflow.com/questions/45299470/lambda-expression-as-inline-data-in-xunit

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!