Skip to content

Commit

Permalink
Add explicit generic type specification to TestCase (#4620)
Browse files Browse the repository at this point in the history
* Initial implementation of TestCase with TypeArgs

* Respond to feedback

* TestCaseSource support + tests

* Make it fail when TypeArgs omitted

* Fix type issues in expected failure test. Rely on nunit API to determine if it fails to run

---------

Co-authored-by: Terje Sandstrom <terje@hermit.no>
  • Loading branch information
stevenaw and OsirisTerje committed Feb 19, 2024
1 parent d96485a commit 829e304
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 2 deletions.
12 changes: 11 additions & 1 deletion src/NUnitFramework/framework/Attributes/TestCaseAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ public bool Explicit
/// </summary>
public string? ExcludePlatform { get; set; }

/// <summary>
/// Get or set the type arguments for a generic test method.
/// If not set explicitly, the generic types will be inferred
/// based on the test case parameters.
/// </summary>
public Type[]? TypeArgs { get; set; } = null;

/// <summary>
/// Gets and sets the category for this test case.
/// May be a comma-separated list of categories.
Expand Down Expand Up @@ -300,7 +307,10 @@ private TestCaseParameters GetParametersForTestCase(IMethodInfo method)
int argsNeeded = parameters.Length;
int argsProvided = Arguments.Length;

parms = new TestCaseParameters(this);
parms = new TestCaseParameters(this)
{
TypeArgs = TypeArgs
};

// Special handling for ExpectedResult (see if it needs to be converted into method return type)
if (parms.HasExpectedResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,13 @@ private static bool CheckTestMethodSignature(TestMethod testMethod, MethodInfoCa

if (testMethod.Method.IsGenericMethodDefinition)
{
if (arglist is null || !new GenericMethodHelper(testMethod.Method.MethodInfo).TryGetTypeArguments(arglist, out var typeArguments))
var typeArguments = parms?.TypeArgs;

if (typeArguments is null && (
arglist is null || !new GenericMethodHelper(testMethod.Method.MethodInfo).TryGetTypeArguments(arglist, out typeArguments)))
{
return MarkAsNotRunnable(testMethod, "Unable to determine type arguments for method");
}

testMethod.Method = testMethod.Method.MakeGenericMethod(typeArguments);
parameters = testMethod.Method.GetParameters();
Expand Down
7 changes: 7 additions & 0 deletions src/NUnitFramework/framework/Internal/TestCaseParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,12 @@ public TestCaseParameters(ITestCaseData data) : base(data)
public bool HasExpectedResult { get; set; }

#endregion

/// <summary>
/// Get or set the type arguments for a generic test method.
/// If not set explicitly, the generic types will be inferred
/// based on the test case parameters.
/// </summary>
public Type[]? TypeArgs { get; set; } = null;
}
}
5 changes: 5 additions & 0 deletions src/NUnitFramework/testdata/TestCaseAttributeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,10 @@ public void MethodWithExcludeRuntime(int num)
public void MethodWithArrayArguments(object o)
{
}

[TestCase("doesn't work", TypeArgs = new[] { typeof(int) })]
public static void MethodWithIncompatibleTypeArgs<T>(T input)
{
}
}
}
13 changes: 13 additions & 0 deletions src/NUnitFramework/testdata/TestCaseSourceAttributeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ public void MethodWithArrayArguments(object o)
{
}

[TestCaseSource(nameof(IncompatibleGenericTypeAndArgumentTestCases))]
public static void MethodWithIncompatibleGenericTypeAndArgument<T>(T o)
{
}

private static IEnumerable ExceptionSource
{
get
Expand Down Expand Up @@ -190,6 +195,14 @@ private static IEnumerable<TestCaseData> ComplexArrayBasedTestInputTestCases()
yield return new TestCaseData(args: new[] { argumentValue });
}

public static IEnumerable<TestCaseData> IncompatibleGenericTypeAndArgumentTestCases()
{
yield return new TestCaseData("doesn't work")
{
TypeArgs = new[] { typeof(int) }
};
}

#region Test name tests

[TestCaseSource(nameof(TestCaseNameTestDataSource))]
Expand Down
47 changes: 47 additions & 0 deletions src/NUnitFramework/tests/Attributes/TestCaseAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -748,5 +748,52 @@ public async Task<T> TestWithAsyncGenericReturnType<T>(T arg1)
}

#endregion

[TestCase(TypeArgs = new[] { typeof(int) })]
public void ExplicitTypeArgsWithoutParameters<T>()
{
Assert.That(typeof(T), Is.EqualTo(typeof(int)));
}

[TestCase("2", TypeArgs = new[] { typeof(long) })]
public void ExplicitTypeArgsWithUnrelatedParameters<T>(string input)
{
Assert.That(typeof(T), Is.EqualTo(typeof(long)));
Assert.That(input, Is.EqualTo("2"));
}

[TestCase(2, TypeArgs = new[] { typeof(long) }, ExpectedResult = typeof(long))]
[TestCase(2L, TypeArgs = new[] { typeof(long) }, ExpectedResult = typeof(long))]
[TestCase(2, ExpectedResult = typeof(int))]
[TestCase(2L, ExpectedResult = typeof(long))]
public Type GenericMethodAndParameterWithExplicitOrImplicitTyping<T>(T input)
=> typeof(T);

[Test]
public void ExplicitTypeArgsWithUnassignableParametersFailsAtRuntime()
{
var suite = TestBuilder.MakeParameterizedMethodSuite(
typeof(TestCaseAttributeFixture),
nameof(TestCaseAttributeFixture.MethodWithIncompatibleTypeArgs));

var test = (Test)suite.Tests[0];

Assert.That(test.RunState, Is.EqualTo(RunState.Runnable));

var result = TestBuilder.RunTest(test);

Assert.That(result.FailCount, Is.EqualTo(1));
Assert.That(result.Message, Does.Contain("Object of type 'System.String' cannot be converted to type 'System.Int32'."));
}

[TestCase(2, TypeArgs = new[] { typeof(long) })]
public void ExplicitTypeArgsWithGenericConstraintSatisfied<T>(int input)
where T : IConvertible
{
var convertedValue = ((IConvertible)input).ToType(typeof(T), null);

Assert.That(convertedValue, Is.TypeOf<T>());
Assert.That(convertedValue, Is.Not.TypeOf(input.GetType()));
}
}
}
80 changes: 80 additions & 0 deletions src/NUnitFramework/tests/Attributes/TestCaseSourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,86 @@ public void TestNameIntrospectsArrayValues()
});
}

[TestCaseSource(nameof(ExplicitTypeArgsWithUnrelatedParametersTestCases))]
public void ExplicitTypeArgsWithUnrelatedParameters<T>(string input)
{
Assert.That(typeof(T), Is.EqualTo(typeof(long)));
Assert.That(input, Is.EqualTo("2"));
}

private static IEnumerable<TestCaseData> ExplicitTypeArgsWithUnrelatedParametersTestCases()
{
yield return new TestCaseData("2") { TypeArgs = new[] { typeof(long) } };
}

[TestCaseSource(nameof(GenericMethodAndParameterWithExplicitOrImplicitTypingTestCases))]
public Type GenericMethodAndParameterWithExplicitOrImplicitTyping<T>(T input)
=> typeof(T);

private static IEnumerable<TestCaseData> GenericMethodAndParameterWithExplicitOrImplicitTypingTestCases()
{
yield return new TestCaseData(2)
{
TypeArgs = new[] { typeof(long) },
ExpectedResult = typeof(long)
};
yield return new TestCaseData(2L)
{
TypeArgs = new[] { typeof(long) },
ExpectedResult = typeof(long)
};
yield return new TestCaseData(2)
{
ExpectedResult = typeof(int)
};
yield return new TestCaseData(2L)
{
ExpectedResult = typeof(long)
};
}

[Test]
public void ExplicitTypeArgsWithUnassignableParametersFailsAtRuntime()
{
var suite = TestBuilder.MakeParameterizedMethodSuite(
typeof(TestCaseSourceAttributeFixture),
nameof(TestCaseSourceAttributeFixture.MethodWithIncompatibleGenericTypeAndArgument));

var test = (Test)suite.Tests[0];

Assert.That(test.RunState, Is.EqualTo(RunState.Runnable));

var result = TestBuilder.RunTest(test);

Assert.That(result.FailCount, Is.EqualTo(1));
Assert.That(result.Message, Does.Contain("Object of type 'System.String' cannot be converted to type 'System.Int32'."));
}

[TestCaseSource(nameof(ExplicitTypeArgsWithGenericConstraintSatisfiedTestCases))]
public void ExplicitTypeArgsWithGenericConstraintSatisfied<T1, T2>(T1 a, T2 b)
where T1 : IComparer<T2>
{
Assert.That(typeof(T1), Is.EqualTo(typeof(IntConverter)));
Assert.That(a, Is.TypeOf<DerivedIntConverter>());
}

public class IntConverter : IComparer<int>
{
public int Compare(int x, int y) => x - y;
}

public class DerivedIntConverter : IntConverter
{
}

private static IEnumerable<TestCaseData> ExplicitTypeArgsWithGenericConstraintSatisfiedTestCases()
{
yield return new TestCaseData(new DerivedIntConverter(), 2)
{
TypeArgs = new[] { typeof(IntConverter), typeof(int) }
};
}

#region Sources used by the tests
private static readonly object[] MyData = new object[]
{
Expand Down

0 comments on commit 829e304

Please sign in to comment.