nunit.analyzers icon indicating copy to clipboard operation
nunit.analyzers copied to clipboard

Strongly typed test data?

Open mariusz96 opened this issue 1 year ago • 7 comments

HI,

What is NUnit way of providing strongly typed test data without declaring custom class for each test?

I mean in simple cases like this:

public class OrderServiceTest
{
    public async Task ValidatesOrder_DuringCreation(Customer customer, IEnumerable<Product> products, Payment payment)
    {
        OrderService service = GetOrderServiceFromSomewhere();

        Result<Order> result = await service.CreateOrderAsync(customer, products, payment);

        Assert.IsTrue(result.Success);
    }

    // Other tests
}

Quick search around google did not yielded much so I wrote my own classes though there's surely must be something built-in I'm missing?

public class OrderServiceTest
{
    [TestCaseSource(nameof(ValidData))]
    public async Task ValidatesOrder_DuringCreation(Customer customer, IEnumerable<Product> products, Payment payment)
    {
        // ...
    }

    public IEnumerable<TestCaseData<Customer, IEnumerable<Product>, Payment>> ValidData()
    {
        yield return new TestCaseData<Customer, IEnumerable<Product>, Payment>(
            new CustomerBuilder().Build(),
            Enumerable.Range(0, 2).Select(_ => new ProductBuilder().Build()),
            new PaymentBuilder.Build());

        yield return new TestCaseData<Customer, IEnumerable<Product>, Payment>(
            new CustomerBuilder().Build(),
            Enumerable.Range(0, 2).Select(_ => new ProductBuilder().Build()),
            new ShippingBuilder.Build()); // does not compile
    }

    // Other tests
}
/// <inheritdoc/>
public class TestCaseData<T> : TestCaseData
{
    public TestCaseData(T arg)
        : base(arg)
    {
    }
}

/// <inheritdoc/>
public class TestCaseData<T1, T2> : TestCaseData
{
    public TestCaseData(T1 arg1, T2 arg2)
        : base(arg1, arg2)
    {
    }
}

/// <inheritdoc/>
public class TestCaseData<T1, T2, T3> : TestCaseData
{
    public TestCaseData(T1 arg1, T2 arg2, T3 arg3)
        : base(arg1, arg2, arg3)
    {
    }
}

// other Ts

mariusz96 avatar Jun 02 '23 16:06 mariusz96

You don't actually need to use any strong typing. The TestCaseData will not care what you send in, and all that is required is that the types match the parameters of the test method. You actually don't even need the TestCaseData class, it is there just for being able to add other attributes to the data (https://docs.nunit.org/articles/nunit/writing-tests/TestCaseData.html). Also, see https://docs.nunit.org/articles/nunit/writing-tests/attributes/testcasesource.html . If you need it for making your own tests more readable, then you of course can make your own classes for that. Note, that you also can derive classes from the TestCaseParameters class. Your generic classes above, as they look now, don't add anything to the type safety, as they take any type of parameters, but that is just the that the NUnit TestCaseData do. But I assume you have further plans for it :-)

OsirisTerje avatar Jun 03 '23 11:06 OsirisTerje

I saw the docs. However, after opening two test suites - one XUnit-based and the other one NUnit-based I was looking for something similar to https://andrewlock.net/creating-strongly-typed-xunit-theory-test-data-with-theorydata/ but could not find anything.

And as I understand from your answer nothig like that exists in the framework. Too bad, it would at least help to ensure at the method level that all argument arrays are of the correct type. I mean ideally, it would not compile if there's a mismatch with the test method but without any typing at all it's all C# 1.0 and runtime exceptions.

Which is not ideal.

mariusz96 avatar Jun 05 '23 09:06 mariusz96

@mariusz96 The Nunit analyzers are currently shipped in a separate package, nunit.analyzers. I think rule NUnit1030 may cover what you're looking for: https://docs.nunit.org/articles/nunit-analyzers/NUnit1030.html

Can you see if that works for you?

There's talk about including these analyzers within the framework itself in the future to make things "just work" a bit more naturally.

EDIT: I see the documentation there mentions "that the current implementation only works for single parameters"

stevenaw avatar Jun 05 '23 12:06 stevenaw

Oh yes, this is what I wanted. I can see it work for me with some records:

private static readonly IEnumerable<IsNUnitData> NUnitNameSpaces = new IsNUnitData[]
{
    new IsNUnitData(1001, ".NUnit"),
    new IsNUnitData(1002, ".NUnitExtensions")
};

public record IsNUnitData(int Id, string Name);

[TestCaseSource(nameof(NUnitNameSpaces))]
public void IsNUnit(IsNUnitData n)
{
    StringAssert.Contains(".NUnit", n.Name);
}

I guess it'll have to do for now, thank you.

mariusz96 avatar Jun 05 '23 13:06 mariusz96

Glad it helped @mariusz96 !

And thank you for the question. It's likely an area which could be improved, but would likely require changes coordinated between the framework and the analyzers codebases.

I'm going to label this an idea for consideration for future improvement. @nunit/framework-team @nunit/analyzers-team what are your thoughts on validating test case source parameters when passed as multiple arguments to the test method?

stevenaw avatar Jun 05 '23 23:06 stevenaw

This can only be improved in the analyzer as the compiler has no knowledge that the attribute has anything to do with the parameters of the method. Even the xunit example mentioned will not give compile time errors unless the xunit analyzer does check this.

I want to harmonize the different Source attributes as the sources have the same requirement. At the moment everything winds up as 'object?[] Argument.

manfred-brands avatar Jun 06 '23 10:06 manfred-brands

@manfred-brands is right (also xunit analyzers does not check this - at least they didn't the last time I checked).

mikkelbu avatar Jun 06 '23 21:06 mikkelbu