Do developers dream of declarative tests

Completion of work on
last publication
(which it is not necessary to read to understand this one) brought me not peace, but
sword
dream of peace. A world where you can write more expressive strongly typed tests and instead of
[TestCase(typeof(Impl), "command")]
public void Test(Type impl, string cmd) =>
((I)Activator.CreateInstance(impl)).Do(cmd);
use
[TestCase<Impl>("command")]
public void Test<TImpl>(string cmd) where TImpl : I, new() =>
new TImpl().Do(cmd);
And he was closer than I thought. And then it went and went…
First, let’s create a Class Library project in Visual Studio (I use 2022) with links to the necessary libraries:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
</ItemGroup>
</Project>
Let’s add a simple test and make sure it passes:
using NUnit.Framework;
using System;
public class A { }
public class B : A { }
public class C : A { }
public class D : A { }
[TestFixture]
public class SampleTests
{
[TestCase(typeof(B), typeof(A), true)]
[TestCase(typeof(C), typeof(A), true)]
[TestCase(typeof(C), typeof(B), false)]
public void Test(Type tSub, Type tSuper, bool expected)
{
var actual = tSub.IsAssignableTo(tSuper);
Assert.AreEqual(expected, actual);
}
}
Then we change the test like this:
[TestCase(typeof(B), typeof(A), true)]
[TestCase(typeof(C), typeof(A), true)]
[TestCase(typeof(C), typeof(B), false)]
public void Test<TSub, TSuper>(bool expected)
where TSub : A
where TSuper : A
{
var actual = typeof(TSub).IsAssignableTo(typeof(TSuper));
Assert.AreEqual(expected, actual);
}

Create an attribute
GenericCaseAttribute
inheriting it from
TestCaseAttribute
and re-implementing the interface
ITestBuilder
:
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
public class GenericCaseAttribute : TestCaseAttribute, ITestBuilder
{
private readonly IReadOnlyCollection<Type> typeArguments;
public GenericCaseAttribute(Type[] typeArguments, params object[] arguments)
: base(arguments) => this.typeArguments = typeArguments.ToArray();
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite) =>
base.BuildFrom(
method.IsGenericMethodDefinition || typeArguments.Any() ?
method.MakeGenericMethod(typeArguments.ToArray()) :
method,
suite);
}
And use it instead
TestCaseAttribute
in the falling test:
[GenericCase(new[] { typeof(B), typeof(A) }, true)]
[GenericCase(new[] { typeof(C), typeof(A) }, true)]
[GenericCase(new[] { typeof(C), typeof(B) }, false)]
public void Test<TSub, TSuper>(bool expected)
where TSub : A
where TSuper : A
{
var actual = typeof(TSub).IsAssignableTo(typeof(TSuper));
Assert.AreEqual(expected, actual);
}

Now let’s diversify the tests like this:
public interface I1 { }
public interface I2 { }
public interface I3 { }
public interface I4 { }
public class A : I1, I2 { }
public class B : A, I3 { }
public class C : A, I3 { }
public class D : A, I4 { }
[TestFixture]
public class SampleTests
{
[GenericCase(new Type[] { }, false)]
[GenericCase(new[] { typeof(A) }, false)]
[GenericCase(new[] { typeof(C), typeof(B), typeof(A) }, false)]
[GenericCase(new[] { typeof(B), typeof(A) }, true)]
[GenericCase(new[] { typeof(C), typeof(B) }, false)]
[GenericCase(new[] { typeof(D), typeof(A) }, false)]
public void Test<TSub, TSuper>(bool expected)
where TSub : A, I3
where TSuper : I1, I2
{
var actual = typeof(TSub).IsAssignableTo(typeof(TSuper));
Assert.AreEqual(expected, actual);
}
[GenericCase(new Type[] { })]
[GenericCase(new[] { typeof(object) })]
public void Test() { }
}

This is due to an exception caused by an incompatibility between attribute argument types and test method parameter types. The problem can be solved somehow like this:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
if (IsIncompatible(method))
{
// return ...
}
return base.BuildFrom(
method.IsGenericMethodDefinition || typeArguments.Any() ?
method.MakeGenericMethod(typeArguments.ToArray()) :
method,
suite);
}
But when checking compatibility, it’s easy to miss some nuance and get a crooked bike (I’ve got a couple of times). Since
IMethodInfo IMethodInfo.MakeGenericMethod(params Type[])
he himself checks everything better than us, let’s leave it to him:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
try
{
return base.BuildFrom(
method.IsGenericMethodDefinition || typeArguments.Any() ?
method.MakeGenericMethod(typeArguments.ToArray()) :
method,
suite);
}
catch (Exception ex)
{
return base.BuildFrom(method, suite).SetNotRunnable(ex.Message);
}
}
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using System.Collections.Generic;
public static class TestMethodExtensions
{
public static IEnumerable<TestMethod> SetNotRunnable(this IEnumerable<TestMethod> tests, string message)
{
foreach(var test in tests)
yield return test.SetNotRunnable(message);
}
public static TestMethod SetNotRunnable(this TestMethod test, string message)
{
test.RunState = RunState.NotRunnable;
test.Properties.Set(PropertyNames.SkipReason, message);
return test;
}
}
Now another matter.



Chapter 2
Dance like no one is watching. Sing like no one is listening. Use preview features as if they were already in the release. It seems that the last advice is not the best, but my hands itch, so we put a daw in Tool > Options > Environment > Preview Features > Use previews of the .NET SDK (requires restart)
and select language version preview
.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>preview</LangVersion> <!--enable generic attributes-->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
</ItemGroup>
</Project>
AT
GenericCaseAttribute
slightly change the constructor:
public GenericCaseAttribute(params object[] arguments)
: base(arguments) => typeArguments = GetType().GetGenericArguments();
Adding generic attributes:
public class GenericCaseAttribute<T> : GenericCaseAttribute
{
public GenericCaseAttribute(params object[] arguments)
: base(arguments) { }
}
public class GenericCaseAttribute<T1, T2> : GenericCaseAttribute
{
public GenericCaseAttribute(params object[] arguments)
: base(arguments) { }
}
public class GenericCaseAttribute<T1, T2, T3> : GenericCaseAttribute
{
public GenericCaseAttribute(params object[] arguments)
: base(arguments) { }
}
And we use them in tests:
[GenericCase(false)]
[GenericCase<A>(false)]
[GenericCase<C, B, A>(false)]
[GenericCase<B, A>(true)]
[GenericCase<C, B>(false)]
[GenericCase<D, A>(false)]
public void Test<TSub, TSuper>(bool expected)
where TSub : A, I3
where TSuper : I1, I2
{
var actual = typeof(TSub).IsAssignableTo(typeof(TSuper));
Assert.AreEqual(expected, actual);
}
[GenericCase]
[GenericCase<object>]
public void Test() { }
Hooray! Works!
Now let’s try a more interesting example. Abstracting from the details, in the last publication (which prompted me to these fantasies) there was something about executable scripts
IScript
.
public interface IScript
{
void Execute();
}
Which can be checked by validators
IValidator
.
public interface IValidator
{
void Validate(IScript script);
}
Before execution inside the executor
Executor
.
public class Executor
{
readonly IValidator validator;
public Executor(IValidator validator) =>
this.validator = validator;
public void Execute(IScript script)
{
validator.Validate(script);
script.Execute();
}
}
In this case, you can change some important data
Data
.
public class Data
{
public bool IsChanged { get; private set; }
public void Change() =>
IsChanged = true;
}
located in storage
Store
.
public static class Store
{
private static readonly Dictionary<string, Data> store = new();
public static Data GetData(string id) =>
store.TryGetValue(id, out var data) ? data : (store[id] = new());
}
Safe scripts
HarmlessScript
don’t try to change them.
public class HarmlessScript : IScript
{
void IScript.Execute() { }
}
Unlike attacks.
Attack
which are common
OrdinaryAttack
advanced
AdvancedAttack
and excellent
SuperiorAttack
.
public abstract class Attack : IScript
{
void IScript.Execute() =>
Store.GetData($"{GetHashCode()}").Change();
}
public class OrdinaryAttack : Attack { }
public class AdvancedAttack : OrdinaryAttack { }
public class SuperiorAttack : AdvancedAttack { }
An ordinary validator is called upon to resist them.
OrdinaryValidator
capable of repelling only a normal attack, and an advanced
AdvancedValidator
capable of stopping even an advanced one, respectively.
public class OrdinaryValidator : IValidator
{
void IValidator.Validate(IScript script)
{
if (script is Attack && script is not AdvancedAttack)
throw new Exception("Attack detected.");
}
}
public class AdvancedValidator : IValidator
{
void IValidator.Validate(IScript script)
{
if (script is Attack && script is not SuperiorAttack)
throw new Exception("Attack detected.");
}
}
The interaction of these entities was verified by tests:
using NUnit.Framework;
using System;
[TestFixture]
public class DemoTests
{
[TestCase(typeof(OrdinaryValidator), typeof(HarmlessScript), true, false)]
[TestCase(typeof(AdvancedValidator), typeof(HarmlessScript), true, false)]
[TestCase(typeof(OrdinaryValidator), typeof(OrdinaryAttack), false, false)]
[TestCase(typeof(AdvancedValidator), typeof(OrdinaryAttack), false, false)]
[TestCase(typeof(OrdinaryValidator), typeof(AdvancedAttack), true, true)]
[TestCase(typeof(AdvancedValidator), typeof(AdvancedAttack), false, false)]
[TestCase(typeof(OrdinaryValidator), typeof(SuperiorAttack), true, true)]
[TestCase(typeof(AdvancedValidator), typeof(SuperiorAttack), true, true)]
public void Test(Type validatorType, Type scriptType, bool hasExecuted, bool dataChanged)
{
// Arrange
IValidator validator = (IValidator)Activator.CreateInstance(validatorType);
IScript script = (IScript)Activator.CreateInstance(scriptType);
// Act
Exception exception = default;
try
{
new Executor(validator).Execute(script);
}
catch (Exception e)
{
exception = e;
}
// Asert
Assert.AreEqual(hasExecuted, exception is null);
Assert.AreEqual(dataChanged, Store.GetData($"{script.GetHashCode()}").IsChanged);
}
}
Now let’s create a separate entity
ICheck
to split the check that the script was executed
HasExecuted
and checking for data changes
DataChanged
.
public interface ICheck
{
bool Check(IValidator validator, IScript script);
}
public class HasExecuted : ICheck
{
public bool Check(IValidator validator, IScript script)
{
try
{
new Executor(validator).Execute(script);
return true;
}
catch
{
return false;
}
}
}
public class DataChanged : ICheck
{
public bool Check(IValidator validator, IScript script)
{
try
{
new Executor(validator).Execute(script);
}
catch
{
}
return Store.GetData($"{script.GetHashCode()}").IsChanged;
}
}
And we use it to rewrite the tests:
[TestCase(typeof(OrdinaryValidator), typeof(HarmlessScript), typeof(HasExecuted), true)]
[TestCase(typeof(OrdinaryValidator), typeof(HarmlessScript), typeof(DataChanged), false)]
[TestCase(typeof(AdvancedValidator), typeof(HarmlessScript), typeof(HasExecuted), true)]
[TestCase(typeof(AdvancedValidator), typeof(HarmlessScript), typeof(DataChanged), false)]
[TestCase(typeof(OrdinaryValidator), typeof(OrdinaryAttack), typeof(HasExecuted), false)]
[TestCase(typeof(OrdinaryValidator), typeof(OrdinaryAttack), typeof(DataChanged), false)]
[TestCase(typeof(AdvancedValidator), typeof(OrdinaryAttack), typeof(HasExecuted), false)]
[TestCase(typeof(AdvancedValidator), typeof(OrdinaryAttack), typeof(DataChanged), false)]
[TestCase(typeof(OrdinaryValidator), typeof(AdvancedAttack), typeof(HasExecuted), true)]
[TestCase(typeof(OrdinaryValidator), typeof(AdvancedAttack), typeof(DataChanged), true)]
[TestCase(typeof(AdvancedValidator), typeof(AdvancedAttack), typeof(HasExecuted), false)]
[TestCase(typeof(AdvancedValidator), typeof(AdvancedAttack), typeof(DataChanged), false)]
[TestCase(typeof(OrdinaryValidator), typeof(SuperiorAttack), typeof(HasExecuted), true)]
[TestCase(typeof(OrdinaryValidator), typeof(SuperiorAttack), typeof(DataChanged), true)]
[TestCase(typeof(AdvancedValidator), typeof(SuperiorAttack), typeof(HasExecuted), true)]
[TestCase(typeof(AdvancedValidator), typeof(SuperiorAttack), typeof(DataChanged), true)]
public void Test(Type validatorType, Type scriptType, Type checkType, bool expected)
{
// Arrange
IValidator validator = (IValidator)Activator.CreateInstance(validatorType);
IScript script = (IScript)Activator.CreateInstance(scriptType);
ICheck check = (ICheck)Activator.CreateInstance(checkType);
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
And then we use
GenericCaseAttribute
:
[GenericCase<OrdinaryValidator, HarmlessScript, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, HarmlessScript, DataChanged>(false)]
[GenericCase<AdvancedValidator, HarmlessScript, HasExecuted>(true)]
[GenericCase<AdvancedValidator, HarmlessScript, DataChanged>(false)]
[GenericCase<OrdinaryValidator, OrdinaryAttack, HasExecuted>(false)]
[GenericCase<OrdinaryValidator, OrdinaryAttack, DataChanged>(false)]
[GenericCase<AdvancedValidator, OrdinaryAttack, HasExecuted>(false)]
[GenericCase<AdvancedValidator, OrdinaryAttack, DataChanged>(false)]
[GenericCase<OrdinaryValidator, AdvancedAttack, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, AdvancedAttack, DataChanged>(true)]
[GenericCase<AdvancedValidator, AdvancedAttack, HasExecuted>(false)]
[GenericCase<AdvancedValidator, AdvancedAttack, DataChanged>(false)]
[GenericCase<OrdinaryValidator, SuperiorAttack, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, SuperiorAttack, DataChanged>(true)]
[GenericCase<AdvancedValidator, SuperiorAttack, HasExecuted>(true)]
[GenericCase<AdvancedValidator, SuperiorAttack, DataChanged>(true)]
public void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
In my opinion, it is nice and corresponds to the form given at the beginning of the publication.
public void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new() =>
Assert.AreEqual(expected, new TCheck().Check(new TValidator(), new TScript()));
Chapter 3
Carefully!!! Further research of the author may be a perversion!
Suppose we need to separate tests by implementation IScript
.
[GenericCase<OrdinaryValidator, HarmlessScript, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, HarmlessScript, DataChanged>(false)]
[GenericCase<AdvancedValidator, HarmlessScript, HasExecuted>(true)]
[GenericCase<AdvancedValidator, HarmlessScript, DataChanged>(false)]
public void TestHarmlessScript<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
[GenericCase<OrdinaryValidator, OrdinaryAttack, HasExecuted>(false)]
[GenericCase<OrdinaryValidator, OrdinaryAttack, DataChanged>(false)]
[GenericCase<AdvancedValidator, OrdinaryAttack, HasExecuted>(false)]
[GenericCase<AdvancedValidator, OrdinaryAttack, DataChanged>(false)]
public void TestOrdinaryAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
[GenericCase<OrdinaryValidator, AdvancedAttack, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, AdvancedAttack, DataChanged>(true)]
[GenericCase<AdvancedValidator, AdvancedAttack, HasExecuted>(false)]
[GenericCase<AdvancedValidator, AdvancedAttack, DataChanged>(false)]
public void TestAdvancedAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
[GenericCase<OrdinaryValidator, SuperiorAttack, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, SuperiorAttack, DataChanged>(true)]
[GenericCase<AdvancedValidator, SuperiorAttack, HasExecuted>(true)]
[GenericCase<AdvancedValidator, SuperiorAttack, DataChanged>(true)]
public void TestSuperiorAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
But you can fix this by highlighting the method
void Test<TValidator, TScript, TCheck>(bool)
:
[GenericCase<OrdinaryValidator, HarmlessScript, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, HarmlessScript, DataChanged>(false)]
[GenericCase<AdvancedValidator, HarmlessScript, HasExecuted>(true)]
[GenericCase<AdvancedValidator, HarmlessScript, DataChanged>(false)]
public void TestHarmlessScript<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
Test<TValidator, TScript, TCheck>(expected);
}
[GenericCase<OrdinaryValidator, OrdinaryAttack, HasExecuted>(false)]
[GenericCase<OrdinaryValidator, OrdinaryAttack, DataChanged>(false)]
[GenericCase<AdvancedValidator, OrdinaryAttack, HasExecuted>(false)]
[GenericCase<AdvancedValidator, OrdinaryAttack, DataChanged>(false)]
public void TestOrdinaryAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
Test<TValidator, TScript, TCheck>(expected);
}
[GenericCase<OrdinaryValidator, AdvancedAttack, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, AdvancedAttack, DataChanged>(true)]
[GenericCase<AdvancedValidator, AdvancedAttack, HasExecuted>(false)]
[GenericCase<AdvancedValidator, AdvancedAttack, DataChanged>(false)]
public void TestAdvancedAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
Test<TValidator, TScript, TCheck>(expected);
}
[GenericCase<OrdinaryValidator, SuperiorAttack, HasExecuted>(true)]
[GenericCase<OrdinaryValidator, SuperiorAttack, DataChanged>(true)]
[GenericCase<AdvancedValidator, SuperiorAttack, HasExecuted>(true)]
[GenericCase<AdvancedValidator, SuperiorAttack, DataChanged>(true)]
public void TestSuperiorAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
Test<TValidator, TScript, TCheck>(expected);
}
private void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
Is it possible to get rid of his call?
Create an attribute
DeclarativeCaseAttribute<TValidator, TScript, TCheck>
in which we re-implement
ITestBuilder
and also transfer into it
void TestSuperiorAttack<TValidator, TScript, TCheck>(bool)
from test class:
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using System.Collections.Generic;
public class DeclarativeCaseAttribute<TValidator, TScript, TCheck>
: GenericCaseAttribute<TValidator, TScript, TCheck>, ITestBuilder
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
public DeclarativeCaseAttribute(params object[] arguments)
: base(arguments) { }
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
return base.BuildFrom(method, suite);
}
private void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
}
The tests now do nothing and look like this:
[DeclarativeCase<OrdinaryValidator, HarmlessScript, HasExecuted>(true)]
[DeclarativeCase<OrdinaryValidator, HarmlessScript, DataChanged>(false)]
[DeclarativeCase<AdvancedValidator, HarmlessScript, HasExecuted>(true)]
[DeclarativeCase<AdvancedValidator, HarmlessScript, DataChanged>(false)]
public void TestHarmlessScript<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
}
[DeclarativeCase<OrdinaryValidator, OrdinaryAttack, HasExecuted>(false)]
[DeclarativeCase<OrdinaryValidator, OrdinaryAttack, DataChanged>(false)]
[DeclarativeCase<AdvancedValidator, OrdinaryAttack, HasExecuted>(false)]
[DeclarativeCase<AdvancedValidator, OrdinaryAttack, DataChanged>(false)]
public void TestOrdinaryAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
}
[DeclarativeCase<OrdinaryValidator, AdvancedAttack, HasExecuted>(true)]
[DeclarativeCase<OrdinaryValidator, AdvancedAttack, DataChanged>(true)]
[DeclarativeCase<AdvancedValidator, AdvancedAttack, HasExecuted>(false)]
[DeclarativeCase<AdvancedValidator, AdvancedAttack, DataChanged>(false)]
public void TestAdvancedAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
}
[DeclarativeCase<OrdinaryValidator, SuperiorAttack, HasExecuted>(true)]
[DeclarativeCase<OrdinaryValidator, SuperiorAttack, DataChanged>(true)]
[DeclarativeCase<AdvancedValidator, SuperiorAttack, HasExecuted>(true)]
[DeclarativeCase<AdvancedValidator, SuperiorAttack, DataChanged>(true)]
public void TestSuperiorAttack<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
}
For convenience, let’s simplify
void Test<TValidator, TScript, TCheck>(bool)
before:
private void Test(bool expected)
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
And let’s get to the fun part. Let’s try in
DeclarativeCaseAttribute<TValidator, TScript, TCheck>
replace the tests in such a simple way:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
var @base = this as TestCaseAttribute;
var type = GetType();
var test = type.GetMethod(nameof(Test), BindingFlags.NonPublic | BindingFlags.Instance);
return @base.BuildFrom(
new MethodWrapper(type, test),
new TestFixture(new TypeWrapper(type), Arguments));
}
Tests have changed names, and they all fail with the message
“Method is not public”
, and by double-clicking on the name of the test in the Test Explorer, a transition occurs somewhere in the wrong place. Besides there were any superfluous not started tests. But Output > Tests still displays the correct number of them.

Well, let’s make the method public. Let’s make one more small change:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
var @base = this as TestCaseAttribute;
var type = GetType();
return @base.BuildFrom(
new MethodWrapper(type, nameof(Test)),
new TestFixture(new TypeWrapper(type), Arguments));
}
public void Test(bool expected)
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
The tests fail again, but the message has changed to:
Message:
System.Reflection.TargetException : Object does not match target type.
Stack Trace:
RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
MethodBase.Invoke(Object obj, Object[] parameters)
Reflect.InvokeMethod(MethodInfo method, Object fixture, Object[] args)
It looks like the test runner is trying to call the overridden method on the original test class. But a little street magic solves the problem. Enough to do
void Test(bool)
static, and the tests will work.
For me, this behavior is not obvious, I’m also not sure that it is clearly documented somewhere, so we’ll return to this place.
. For now, let’s add
DeclarativeCaseAttribute<TValidator, TScript, TCheck>
method
string CreateName(TestMethod, Test, IMethodInfo, Func<Test, string>, Func<Type, string>)
and use it:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
var @base = this as TestCaseAttribute;
var type = GetType();
return @base.BuildFrom(
new MethodWrapper(type, nameof(Test)),
new TestFixture(new TypeWrapper(type), Arguments))
.Select(test =>
{
test.FullName = CreateName(test, suite, method,
suite => suite.FullName, type => type.FullName);
test.Name = CreateName(test, suite, method,
suite => suite.Name, type => type.Name);
return test;
});
}
private static readonly IReadOnlyCollection<Type> types = new[]
{
typeof(TValidator),
typeof(TScript),
typeof(TCheck)
};
private static string CreateName(
TestMethod test,
Test suite,
IMethodInfo method,
Func<Test, string> suitNameSelector,
Func<Type, string> typeNameSelector) =>
$"{suitNameSelector(suite)}.{method.Name}<{
string.Join(",", types.Select(typeNameSelector))}>({
string.Join(',', test.Arguments)})";

The tests themselves can be painlessly reduced to:
[DeclarativeCase<OrdinaryValidator, HarmlessScript, HasExecuted>(true)]
[DeclarativeCase<OrdinaryValidator, HarmlessScript, DataChanged>(false)]
[DeclarativeCase<AdvancedValidator, HarmlessScript, HasExecuted>(true)]
[DeclarativeCase<AdvancedValidator, HarmlessScript, DataChanged>(false)]
public void TestHarmlessScript() { }
[DeclarativeCase<OrdinaryValidator, OrdinaryAttack, HasExecuted>(false)]
[DeclarativeCase<OrdinaryValidator, OrdinaryAttack, DataChanged>(false)]
[DeclarativeCase<AdvancedValidator, OrdinaryAttack, HasExecuted>(false)]
[DeclarativeCase<AdvancedValidator, OrdinaryAttack, DataChanged>(false)]
public void TestOrdinaryAttack() { }
[DeclarativeCase<OrdinaryValidator, AdvancedAttack, HasExecuted>(true)]
[DeclarativeCase<OrdinaryValidator, AdvancedAttack, DataChanged>(true)]
[DeclarativeCase<AdvancedValidator, AdvancedAttack, HasExecuted>(false)]
[DeclarativeCase<AdvancedValidator, AdvancedAttack, DataChanged>(false)]
public void TestAdvancedAttack() { }
[DeclarativeCase<OrdinaryValidator, SuperiorAttack, HasExecuted>(true)]
[DeclarativeCase<OrdinaryValidator, SuperiorAttack, DataChanged>(true)]
[DeclarativeCase<AdvancedValidator, SuperiorAttack, HasExecuted>(true)]
[DeclarativeCase<AdvancedValidator, SuperiorAttack, DataChanged>(true)]
public void TestSuperiorAttack() { }
Prank was a success! Now we can write tests without a method body, described by an attribute and contained in it. I have no idea where it can be useful in life, but it looks entertaining.
Epilogue
Let’s go back to the dirty hack with the static method when substituting the test and try to replace it with another solution.
Add an interface IDeclarativeTest
:
public interface IDeclarativeTest
{
void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new();
}
And in
DeclarativeCaseAttribute<TValidator, TScript, TCheck>
we will require its implementation by the test class, so that when replacing the test, it is guaranteed to be able to call the interface method:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
IEnumerable<TestMethod> tests;
var type = suite.TypeInfo.Type;
if (!typeof(IDeclarativeTest).IsAssignableFrom(type))
tests = base.BuildFrom(method, suite)
.SetNotRunnable($"{type} does not implement {typeof(IDeclarativeTest)}.");
else
tests = base.BuildFrom(new MethodWrapper(type, nameof(IDeclarativeTest.Test)), suite);
return tests.Select(test =>
{
test.FullName = CreateName(test, suite, method,
suite => suite.FullName, type => type.FullName);
test.Name = CreateName(test, suite, method,
suite => suite.Name, type => type.Name);
return test;
});
}
For
IDeclarativeTest
create an implementation
DefaultDeclarativeTest
:
using NUnit.Framework;
public class DefaultDeclarativeTest : IDeclarativeTest
{
public void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new()
{
// Arrange
IValidator validator = new TValidator();
IScript script = new TScript();
ICheck check = new TCheck();
// Act
bool actual = check.Check(validator, script);
// Assert
Assert.AreEqual(expected, actual);
}
}
And we use it when implementing
IDeclarativeTest
the test class itself:
using NUnit.Framework;
[TestFixture]
public class DemoTests : IDeclarativeTest
{
public void Test<TValidator, TScript, TCheck>(bool expected)
where TValidator : IValidator, new()
where TScript : IScript, new()
where TCheck : ICheck, new() =>
new DefaultDeclarativeTest().Test<TValidator, TScript, TCheck>(expected);
// Tests...
}
And one moment. If the test method is not empty, then its contents will still not be executed. Therefore, in order to avoid cognitive dissonance in
DeclarativeCaseAttribute<TValidator, TScript, TCheck>
you can prevent it from being applied to non-empty methods:
public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite)
{
IEnumerable<TestMethod> tests;
var type = suite.TypeInfo.Type;
if (!typeof(IDeclarativeTest).IsAssignableFrom(type))
tests = base.BuildFrom(method, suite)
.SetNotRunnable($"{type} does not implement {typeof(IDeclarativeTest)}.");
else if (!method.MethodInfo.IsIdle())
tests = base.BuildFrom(method, suite)
.SetNotRunnable("Method is not idle, i.e. does something.");
else
tests = base.BuildFrom(new MethodWrapper(type, nameof(IDeclarativeTest.Test)), suite);
return tests.Select(test =>
{
test.FullName = CreateName(test, suite, method,
suite => suite.FullName, type => type.FullName);
test.Name = CreateName(test, suite, method,
suite => suite.Name, type => type.Name);
return test;
});
}
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Reflection;
using System.Runtime.CompilerServices;
public static class MethodInfoExtensions
{
private static readonly IReadOnlyCollection<byte> idle = new[]
{
OpCodes.Nop,
OpCodes.Ret
}.Select(code => (byte)code.Value).ToArray();
public static bool IsIdle(this MethodInfo method)
{
var body = method.GetMethodBody();
if (body.LocalVariables.Any())
return false;
if (body.GetILAsByteArray().Except(idle).Any())
return false;
if (method.DeclaringType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
.Any(candidate => IsLocalMethod(candidate, method)))
return false;
return true;
}
private static bool IsLocalMethod(MethodInfo method, MethodInfo container) =>
method.Name.StartsWith($"<{container.Name}>") &&
method.GetCustomAttributes<CompilerGeneratedAttribute>().Any();
}