Search code examples
c#unit-testingmstest

Unit Test throws TargetParameterCountException when using params array


I'm setting up unit tests using Microsoft.VisualStudio.TestTools.UnitTesting test attributes. I'm trying to pass a different number of elements into a params array in my test method, as described in this answer.

Here's the relevant code in my [TestClass]:

[TestClass]
public class ExtensionsTests
{
    [TestMethod]
    [DataRow(1, "")]
    [DataRow(1, "stuff")]
    [DataRow(1, "asdf", "ASDF", "asDF", "ASdF")]
    [DataRow(3, "asdf", "ASDF", "1234", "d1sf5d1f")]
    [DataRow(2, "asdf", "ASDF", "ásdf", "ÁSdF")] //a and á (accent) are different characters 
    public void DbDistinct_ShouldBeCaseInsensitive(int expectedCount, params string[] strs)
    {
        var distinctStuff = strs.DbDistinct();
        Assert.AreEqual(expectedCount, distinctStuff.Count());
    }
}

When I run the unit test, I get this exception before it even make it into the body of the test:

Test method ExtensionsTests.DbDistinct_ShouldBeCaseInsensitive threw exception: System.Reflection.TargetParameterCountException: Parameter count mismatch. at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.ThreadOperations.ExecuteWithAbortSafety(Action action)

Interestingly, when I look at the error for a test case where there is only a single string to be passed into the params array (like the [DataRow(1, "stuff")] test case), I get a different error:

Test method ExtensionsTests.DbDistinct_ShouldBeCaseInsensitive threw exception: System.ArgumentException: Object of type 'System.String' cannot be converted to type 'System.String[]'. at System.RuntimeType.TryChangeType(Object value, Binder binder, CultureInfo culture, Boolean needsSpecialCast) at System.RuntimeType.CheckValue(Object value, Binder binder, CultureInfo culture, BindingFlags invokeAttr) at System.Reflection.MethodBase.CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig) at System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.ThreadOperations.ExecuteWithAbortSafety(Action action)

It looks like the DataRow is ignoring the params part of the test method. How can I specify a variable number of test elements to be passed into my test method? My test project is .NET Framework 4.8 (C# 7.3). I get the same error in both Rider and VS2022, so I don't think this is IDE-related.


Solution

  • Cause of the Exception

    I think figured out why I ran into the Parameter mismatch or the invalid casting issues. The best guess I found based on a linked article on another Stack Overflow answer:

    The DataRow attribute has a constructor that doesn't play well with arrays because the constructor itself accepts params Object[]. This means that if you have an array whose type could fill the role of Object[], the compiler prefers matching your array directly instead of expanding it as part of a params array.

    The suggested solution to explicitly wrap things properly (with a collection initializer) is not compatible with Attributes because everything in an Attribute must be a compile-time constant (and therefore initializers and new don't work).

    My Solution: use [DynamicData]

    I ended up side-stepping this issue by using a DynamicData test cases. I defined a static property that generates my test cases. Each test case is defined as an object[], whose elements are injected as parameters in my test method (in order). I made each test case be an expected value (int) and the test data (string[]):

    public static IEnumerable<object[]> CaseInsensitiveTestCases
    {
        get
        {
            return new[]
            {
                new object[] { 1, new string[] { "" } },
                new object[] { 1, new string[] { "stuff" } },
                new object[] { 1, new string[] { "asdf", "ASDF", "asDF", "ASdF" } },
                new object[] { 3, new string[] { "asdf", "ASDF", "1234", "d1sf5d1f" } },
                new object[] { 2, new string[] { "asdf", "ASDF", "ásdf", "ÁSdF" } }, //a and á (accent) are different characters 
            };
        }
    }
    

    Then I just used [DynamicData] attribute on my unit test method:

    [TestMethod]
    [DynamicData(nameof(CaseInsensitiveTestCases))]
    public void DbDistinct_ShouldBeCaseInsensitive(int expectedCount, string[] strs)
    {
        var distinctStuff = strs.DbDistinct();
        Assert.AreEqual(expectedCount, distinctStuff.Count());
    }