Last active
September 1, 2021 06:39
-
-
Save Keboo/d2f7f470d66fc4ff7d978ccb107b07cf to your computer and use it in GitHub Desktop.
Automatic testing of constructor parameters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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