NUnit TestCase with Generics

后端 未结 9 829
梦毁少年i
梦毁少年i 2021-01-31 13:53

Is there any way to pass generic types using a TestCase to a test in NUnit?

This is what I would like to do but the syntax is not correct...

<         


        
相关标签:
9条回答
  • 2021-01-31 14:29

    I have written my own TestCaseGenericAttribute and TestCaseGenericSourceAttribute. https://github.com/nunit/nunit/issues/3580

    0 讨论(0)
  • 2021-01-31 14:30

    NUnit test methods actually can be generic as long as the generic type arguments can be inferred from parameters:

    [TestCase(42)]
    [TestCase("string")]
    [TestCase(double.Epsilon)]
    public void GenericTest<T>(T instance)
    {
        Console.WriteLine(instance);
    }
    

    If the generic arguments cannot be inferred, the test runner will not have a clue how to resolve type arguments:

    [TestCase(42)]
    [TestCase("string")]
    [TestCase(double.Epsilon)]
    public void GenericTest<T>(object instance)
    {
        Console.WriteLine(instance);
    }
    

    But in this case you can implement a custom attribute:

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class TestCaseGenericAttribute : TestCaseAttribute, ITestBuilder
    {
        public TestCaseGenericAttribute(params object[] arguments)
            : base(arguments)
        {
        }
    
        public Type[] TypeArguments { get; set; }
    
        IEnumerable<TestMethod> ITestBuilder.BuildFrom(IMethodInfo method, Test suite)
        {
            if (!method.IsGenericMethodDefinition)
                return base.BuildFrom(method, suite);
    
            if (TypeArguments == null || TypeArguments.Length != method.GetGenericArguments().Length)
            {
                var parms = new TestCaseParameters { RunState = RunState.NotRunnable };
                parms.Properties.Set("_SKIPREASON", $"{nameof(TypeArguments)} should have {method.GetGenericArguments().Length} elements");
                return new[] { new NUnitTestCaseBuilder().BuildTestMethod(method, suite, parms) };
            }
    
            var genMethod = method.MakeGenericMethod(TypeArguments);
            return base.BuildFrom(genMethod, suite);
        }
    }
    

    Usage:

    [TestCaseGeneric("Some response", TypeArguments = new[] { typeof(IMyInterface), typeof(MyConcreteClass) }]
    public void MyMethod_GenericCall_MakesGenericCall<T1, T2>(string expectedResponse)
    {
        // whatever
    }
    

    And a similar customization for TestCaseSourceAttribute:

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class TestCaseSourceGenericAttribute : TestCaseSourceAttribute, ITestBuilder
    {
        public TestCaseSourceGenericAttribute(string sourceName)
            : base(sourceName)
        {
        }
    
        public Type[] TypeArguments { get; set; }
    
        IEnumerable<TestMethod> ITestBuilder.BuildFrom(IMethodInfo method, Test suite)
        {
            if (!method.IsGenericMethodDefinition)
                return base.BuildFrom(method, suite);
    
            if (TypeArguments == null || TypeArguments.Length != method.GetGenericArguments().Length)
            {
                var parms = new TestCaseParameters { RunState = RunState.NotRunnable };
                parms.Properties.Set("_SKIPREASON", $"{nameof(TypeArguments)} should have {method.GetGenericArguments().Length} elements");
                return new[] { new NUnitTestCaseBuilder().BuildTestMethod(method, suite, parms) };
            }
    
            var genMethod = method.MakeGenericMethod(TypeArguments);
            return base.BuildFrom(genMethod, suite);
        }
    }
    

    Usage:

    [TestCaseSourceGeneric(nameof(mySource)), TypeArguments = new[] { typeof(IMyInterface), typeof(MyConcreteClass) }]
    
    0 讨论(0)
  • 2021-01-31 14:40

    Attributes in C# cannot be generic, so you won't be able to do things exactly as you'd like. Perhaps the easiest thing would be to put TestCase attributes onto a helper method which uses reflection to call the real method. Something like this might work (note, untested):

        [TestCase(typeof(MyClass), "SomeResponse")]
        public void TestWrapper(Type t, string s)
        {
            typeof(MyClassUnderTest).GetMethod("MyMethod_GenericCall_MakesGenericCall").MakeGenericMethod(t).Invoke(null, new [] { s });
        }
    
    0 讨论(0)
  • 2021-01-31 14:46

    I had occasion to do something similar today, and wasn't happy with using reflection.

    I decided to leverage [TestCaseSource] instead by delegating the test logic as a test context to a generic testing class, pinned on a non-generic interface, and called the interface from individual tests (my real tests have many more methods in the interface, and use AutoFixture to set up the context):

    class Sut<T>
    {
        public string ReverseName()
        {
            return new string(typeof(T).Name.Reverse().ToArray());
        }
    }
    
    [TestFixture]
    class TestingGenerics
    {
        public IEnumerable<ITester> TestCases()
        {
            yield return new Tester<string> { Expectation = "gnirtS"};
            yield return new Tester<int> { Expectation = "23tnI" };
            yield return new Tester<List<string>> { Expectation = "1`tsiL" };
        }
    
        [TestCaseSource("TestCases")]
        public void TestReverse(ITester tester)
        {
            tester.TestReverse();
        }
    
        public interface ITester
        {
            void TestReverse();
        }
    
        public class Tester<T> : ITester
        {
            private Sut<T> _sut;
    
            public string Expectation { get; set; }
    
            public Tester()
            {
                _sut=new Sut<T>();
            }
    
            public void TestReverse()
            {
                Assert.AreEqual(Expectation,_sut.ReverseName());
            }
    
        }
    }
    
    0 讨论(0)
  • 2021-01-31 14:46

    Start with the test first--even when testing. What do you want to do? Probably something like this:

    [Test]
    public void Test_GenericCalls()
    {
        MyMethod_GenericCall_MakesGenericCall<int>("an int response");
        MyMethod_GenericCall_MakesGenericCall<string>("a string response");
          :
    }
    

    Then you can just make your test a plain old function test. No [Test] marker.

    public void MyMethod_GenericCall_MakesGenericCall<T>(string expectedResponse)
    {
        // Arrange
    
        // Act
        var response = MyClassUnderTest.MyMethod<T>();
    
        // Assert
        Assert.AreEqual(expectedResponse, response);
    }
    
    0 讨论(0)
  • 2021-01-31 14:46

    I did something similar last week. Here's what I ended up with:

    internal interface ITestRunner
    {
        void RunTest(object _param, object _expectedValue);
    }
    
    internal class TestRunner<T> : ITestRunner
    {
        public void RunTest(object _param, T _expectedValue)
        {
            T result = MakeGenericCall<T>();
    
            Assert.AreEqual(_expectedValue, result);
        }
        public void RunTest(object _param, object _expectedValue)
        {
            RunTest(_param, (T)_expectedValue);
        }
    }
    

    And then the test itself:

    [Test]
    [TestCase(typeof(int), "my param", 20)]
    [TestCase(typeof(double), "my param", 123.456789)]
    public void TestParse(Type _type, object _param, object _expectedValue)
    {
        Type runnerType = typeof(TestRunner<>);
        var runner = Activator.CreateInstance(runnerType.MakeGenericType(_type));
        ((ITestRunner)runner).RunTest(_param, _expectedValue);
    }
    
    0 讨论(0)
提交回复
热议问题