Skip to content

Instantly share code, notes, and snippets.

@Keboo
Last active Sep 1, 2021
Embed
What would you like to do?
Automatic testing of constructor parameters
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Moq;
using Xunit;
namespace Namespace
{
public interface IConstructorTest
{
void Run();
}
public static class ConstructorTest
{
private class Test : IConstructorTest
{
public Dictionary<Type, object?> SpecifiedValues { get; }
= new Dictionary<Type, object?>();
public List<Type> AllowedNullParameters { get; }
= new List<Type>();
private Type TargetType { get; }
public Test(Type targetType)
{
TargetType = targetType;
}
public Test(Test original)
{
TargetType = original.TargetType;
AllowedNullParameters.AddRange(original.AllowedNullParameters);
foreach (KeyValuePair<Type, object?> kvp in original.SpecifiedValues)
{
SpecifiedValues[kvp.Key] = kvp.Value;
}
}
public void Run()
{
foreach (ConstructorInfo constructor in TargetType.GetConstructors())
{
ParameterInfo[] parameters = constructor.GetParameters();
object?[] parameterValues = parameters
.Select(p => p.ParameterType)
.Select(t =>
{
if (SpecifiedValues.TryGetValue(t, out object? value))
{
return value;
}
Mock mock = (Mock) Activator.CreateInstance(typeof(Mock<>).MakeGenericType(t))!;
return mock.Object;
})
.ToArray();
for (int i = 0; i < parameters.Length; i++)
{
object?[] values = parameterValues.ToArray();
values[i] = null;
if (AllowedNullParameters.Contains(parameters[i].ParameterType) ||
(parameters[i].HasDefaultValue && parameters[i].DefaultValue is null))
{
//NB: no exception thrown
constructor.Invoke(values);
}
else
{
string parameterDisplay = $"'{parameters[i].Name}' ({parameters[i].ParameterType.Name})";
TargetInvocationException ex = Assert.Throws<TargetInvocationException>(new Action(() =>
{
object? rv = constructor.Invoke(values);
throw new Exception($"Expected {nameof(ArgumentNullException)} for null parameter {parameterDisplay} but no exception was thrown");
}));
if (ex.InnerException is ArgumentNullException argumentNullException)
{
Assert.Equal(parameters[i].Name, argumentNullException.ParamName);
}
else
{
throw new Exception($"Thrown argument for {parameterDisplay} was '{ex.InnerException?.GetType().Name}' not {nameof(ArgumentNullException)}.", ex.InnerException);
}
}
}
}
}
}
//NB: The necessity of this method is a code smell
public static IConstructorTest AllowNull<TParameterType>(this IConstructorTest test)
{
if (test is Test internalTest)
{
var newTest = new Test(internalTest);
newTest.AllowedNullParameters.Add(typeof(TParameterType));
return newTest;
}
else
{
throw new ArgumentException("Argument not expected type", nameof(test));
}
}
public static IConstructorTest Use<T>(this IConstructorTest test, T value)
{
if (test is Test internalTest)
{
var newTest = new Test(internalTest);
newTest.SpecifiedValues.Add(typeof(T), value);
return newTest;
}
else
{
throw new ArgumentException("Argument not expected type", nameof(test));
}
}
public static IConstructorTest BuildArgumentNullExceptionsTest<T>()
=> new Test(typeof(T));
public static void AssertArgumentNullExceptions<T>()
=> BuildArgumentNullExceptionsTest<T>().Run();
}
}
[Fact]
public void MyClass_Contructor_ThrowsAppropriateArgumentNullExceptions()
=> ConstructorTest.AssertArgumentNullExceptions<MyClass>();
[Fact]
public void OtherClass_Contructor_ThrowsAppropriateArgumentNullExceptions()
{
ConstructorTest.BuildArgumentNullExceptionsTest<MyOtherClass>()
.AllowNull<IOptions<MyOptions>>()
.Run();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment