Lambda expression as inline data in xUnit
Asked Answered
H

5

11

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?

Heartburn answered 25/7, 2017 at 9:39 Comment(0)
H
10

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.

Heartburn answered 25/7, 2017 at 13:49 Comment(0)
D
11

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));
}
Dabbs answered 25/7, 2017 at 14:56 Comment(3)
While it seems a better solution, I don't like it very much because it will appear as a single test in Test Explorer. I would love to see all the required parameters (like in the answer I provided) instead. Thank you anyway!Heartburn
Really, this seems like a bug in the test explorer.Dabbs
This method does display all tests individually in test explorer. I agree with @Dabbs there must have been a bug at the time of posting.Indisputable
H
10

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.

Heartburn answered 25/7, 2017 at 13:49 Comment(0)
S
4

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);
    }
}
Solemnize answered 28/6, 2018 at 7:59 Comment(0)
D
1

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);
}
Dabbs answered 25/7, 2017 at 9:46 Comment(0)
G
0
public class HrcpDbTests
{    
    [Theory]
    [MemberData(nameof(TestData))]
    public void Test(Expression<Func<bool>> exp)
    {
        // Arrange
        // Act
        // Assert
    }
    
    public static IEnumerable<object[]> TestData 
    {
        get
        {
            Expression<Func<bool>> mockExp1 = () => 1 == 0;
            Expression<Func<bool>> mockExp2 = () => 1 != 2;

            return new List<object[]>
            {
                new object[]
                {
                    mockExp1
                },
                new object[]
                {
                    mockExp2
                }
            }
        }
    }
}
Gnotobiotics answered 12/4, 2021 at 13:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.