Could you provide argument converter interface?
Unfortunately, default conversion methods (Convert.ChangeType, TypeConverter.ConvertFrom) are not enough.
There were cases when I needed "external argument converter" to convert source argument to target type (https://github.com/nunit/nunit/issues/3380, https://github.com/nunit/nunit/issues/3385, https://github.com/nunit/nunit/issues/3571).
There were also cases when I needed "external argument factory" to convert several source arguments to one target argument.
So, could you make your pipeline more customizable and provide interfaces or attributes for arguments conversion? There may only be problems with analyzer.
For example:
public class Tests :
IConverter<string, DataTime>,
IConverter<int[], long[]> {
...
}
[TestCase( "2000:00:00 12:30Z" )]
[Converter( nameof(CreateUtcDateTime) )]
public void Test_00(DateTime value) {
}
[TestCase( "http://www.site.com", "folder" )]
[Factory( nameof(CreateUri) )]
public void Test_00(Uri value) {
}
I just noticed that I can do such:
[TestCaseSource( nameof( GetUri ), new object[] { "http://www.site.com", "folder" } )]
public void Test_00(Uri value) {
TestContext.WriteLine( value );
}
private static IEnumerable<Uri> GetUri(string @base, string relative) {
yield return new Uri( new Uri( @base ), relative );
}
It works. But it'd like to see something like:
[TestCase( "http://www.site.com", "folder", ArgumentsFactory = nameof( GetUri ) )]
public void Test_00(Uri value) {
}
[TestCase( "http://www.site.com", "folder", "MyObject" )]
[ArgumentFactories( nameof( GetUri ), nameof( GetMyObject ) )]
public void Test_01(Uri value, MyObject value2) {
}
Your TestCaseSource is basically a factory for arguments to tests. In fact, many years ago, it was even called a factory, using terminology from mbunit. Back around the 2.4 or 2.5 release I got rid of that name because it seemed to be confusing people.
Is our preference for a different syntax just a matter of style or are there things you find you are unable to do with TestCaseSource.
Generally, we have not stressed conversion for TestCaseSource because your code is able to provide the exact type from the source method, with no conversion needed by NUnit. With TestCase, of course, that's not true because of the limitations of C# in passing arguments to the attribute constructor.
BTW... arguments to TestCaseSource are usually only needed if you are using the same source to generate different arguments for different methods.
TestCaseSource is just not-inline arguments provider. Of course thanks to methodParams we can use it as factory.
But it would be more expressively if we could use TestCase with special factory method as in my example.
P.s. Why do you not use params for methodParams? P.p.s. I had read that parameter is variable and argument is data. So I think methodArgs would be better.
Your suggestion is the kind of thing I'd consider if I were creating a new framework. Could also be part of a major release of NUnit - i.e. NUnit 4 - but I'm not involved in that.
I don't know why the argument is called methodParams. I agree with your observations about the name. That feature is not something I worked on.
I'm marking this as an idea. To be honest, I'm not convinced of the value as it seems to be just a different way of doing things that we are already able to do. If I am misunderstanding, please elaborate a bit. 😄
I think this might be something similar to what I'm after as well. With DDD, it's common to use custom classes to represent value types within a particular domain e.g. currency. These value types often just encapsulate a primitive value (like a double or integer) and provide context for what that value represents and how it ought to interact with other elements of the domain.
In my case, I have a value type in my domain called Period which roughly corresponds to a calendar month (e.g. period 215 is November 2021, 216 is December 2021, 217 is January 2022...). It's essentially a contextualised integer, and accepts an integer in its constructor:
public Period(int value)
{
if (value < (int) StartOfCommissionYear)
throw new ArgumentException($"Periods are not defined before period {(int) StartOfCommissionYear}");
Value = value;
}
I have a lot of tests that are parameterised by periods, and often do something like this:
[Test]
public async Task ShouldReturnActivityHistory_WithLatestPeriod_BasedOnActivePeriod(
[Range(80, 100, 5)] int activePeriod)
{
// Given a certain period is active
var period = new Period(activePeriod);
GivenActivePeriod(period);
// ...
}
It would be pretty neat if I could instead do something like this:
[Test]
public async Task ShouldReturnActivityHistory_WithLatestPeriod_BasedOnActivePeriod(
[Range(80, 100, 5)] Period activePeriod)
{
// Given a certain period is active
GivenActivePeriod(activePeriod);
// ...
}
Not sure what the best way to implement support for this would be. Perhaps first attempting to convert the type the way it already does (I assume with Convert.ChangeType()?) and, if that fails, trying to find a suitable constructor via reflection that accepts the type of value specified in the [Range]/[TestCase]/etc. attributes.