Guide: Extending the framework

Nuclear.TestSite can be extended easily in many ways, to allow for a wide range of test scenarios. Anyone can extend the instruction set of an existing test suite as well as add entirely new specialized test suites to the API. There is also a way to properly test the newly created test instructions, using Nuclear.Test and all of its regular test result processing mechanisms.

Extending a test suite

Every test suite can be extended by adding new test instructions. This is done using extension methods on the specific test suite type. Any custom test instruction must follow a set of rules, for it to play nicely with the rest of the system. An instruction method…

  • must never trigger an exceptional abort of the workflow.
  • must register exactly one test result for every call. This includes ordinary test results and test failures.
  • must validate input data. Invalid data must always trigger a test failure.
  • should return void. It is encouraged to only return data via out parameters.

Every test suite offers public but hidden methods for result processing. The method InternalTest accepts all the relevant data that is necessary to register a new result. Instruction negation is handled further down the line and doesn’t have to be considered. A test instruction with invalid input data or a failure during evaluation must call the method InternalFail. This will register a failed result along with a custom failure message.

public static class StringTestSuiteExtensions {

    public static void IsHelloWorld(this StringTestSuite _this, String @string,
        String customMessage = null, [CallerFilePath] String _file = null, [CallerMemberName] String _method = null) {

        if(@string == null) {
            _this.InternalFail($"Parameter '{nameof(@string)}' is null.", _file, _method);
            return;
        }

        _this.InternalTest(@string == "Hello World!", $"[String = {@string.Format()}]", customMessage, _file, _method);
    }

}

Adding a new test suite

It can make sense to create a completely new test suite. Doing so can make testing of complex types or custom types easier. Every test suite should inherit directly from ChildTestSuite, which adds some basic implementation. The test suite is best implemented as a partial class, splitting the suite implementation and test instructions into separate files. Public members that are not test instructions should be hidden, to allow for a clean intellisense.

// DateTimeTestSuite.cs
[EditorBrowsable(EditorBrowsableState.Never)]
public partial class DateTimeTestSuite : ChildTestSuite {

    internal DateTimeTestSuite(TestSuiteCollection parent) : base(parent) { } 

    [EditorBrowsable(EditorBrowsableState.Never)]
    public void InternalTest(Boolean condition, String message, String customMessage, String file, String method, [CallerMemberName] String testInstruction = null)
        => Parent.InternalTest(condition, message, customMessage, file, method, testInstruction);

    [EditorBrowsable(EditorBrowsableState.Never)]
    public void InternalFail(String message, String file, String method, [CallerMemberName] String testInstruction = null)
        => Parent.InternalFail(message, file, method, testInstruction);

}
// DateTimeTestSuite.Instructions.cs
public partial class DateTimeTestSuite {

    public void IsWeekend(DateTime dateTime,
        String customMessage = null, [CallerFilePath] String _file = null, [CallerMemberName] String _method = null) {

        IList<DayOfWeek> days = new List<DayOfWeek>() {
            DayOfWeek.Saturday,
            DayOfWeek.Sunday
        };

        InternalTest(days.Contains(dateTime.DayOfWeek), $"[Date = {dateTime.Format()}]", customMessage, _file, _method);
    }

    public void IsWorkDay(DateTime dateTime,
        String customMessage = null, [CallerFilePath] String _file = null, [CallerMemberName] String _method = null) {

        IList<DayOfWeek> days = new List<DayOfWeek>() {
            DayOfWeek.Monday,
            DayOfWeek.Tuesday,
            DayOfWeek.Wednesday,
            DayOfWeek.Thursday,
            DayOfWeek.Friday,
            DayOfWeek.Saturday
        };

        InternalTest(days.Contains(dateTime.DayOfWeek), $"[Date = {dateTime.Format()}]", customMessage, _file, _method);
    }

}

The new test suite must be made available to the Nuclear.TestSite API. This means adding it to the class TestSuiteCollection, where all the other test suites are accessed from as well. A property can’t be added, but again extension methods come to the rescue.

public static class TestSuiteCollectionExtensions {

    public static DateTimeTestSuite DateTime(this TestSuiteCollection _this) => new DateTimeTestSuite(_this);

}

Testing custom instructions

To test the newly added test instructions, the package Nuclear.TestSite.Dummies can be added via NuGet. The package contains a range of types that are necessary for this specific test situation. The namespaces Nuclear.TestSite.TestTypes and Nuclear.TestSite.TestComparers offer some relevant interface implementations and types with well-defined failing behaviour for equality related testing.

The static class DummyTest acts as a copy of the regular test suite access class Test. However, it stores the test results in a separate location, so that regular test results and the dummy results do not mix.

The static class Statics contains a range of public methods, that can be used to analyse the state of the dummy result store. It also contains a data driven test helper method called DDTResultState, that invokes a given action and then automatically checks the state of the dummy result store. The method accepts an action – preferably a call to a test instruction on the dummy API – and expected values for the corresponding method results count and value, message and instruction name of the last available test result.

// StringTestSuiteExtensions_uTests.cs
class StringTestSuiteExtensions_uTests {

    [TestMethod]
    [TestParameters(null, 1, false, "Parameter 'string' is null.")]
    [TestParameters("", 2, false, "[String = '']")]
    [TestParameters(" ", 3, false, "[String = ' ']")]
    [TestParameters("Not Hello World!", 4, false, "[String = 'Not Hello World!']")]
    [TestParameters("Hello World!", 5, true, "[String = 'Hello World!']")]
    void IsHelloWorld(String input1, Int32 count, Boolean result, String message) {

        Statics.DDTResultState(() => DummyTest.If.String.IsHelloWorld(input1),
            (count, result, message), "Test.If.String.IsHelloWorld");

    }

    [TestMethod]
    [TestParameters(null, 1, false, "Parameter 'string' is null.")]
    [TestParameters("", 2, true, "[String = '']")]
    [TestParameters(" ", 3, true, "[String = ' ']")]
    [TestParameters("Not Hello World!", 4, true, "[String = 'Not Hello World!']")]
    [TestParameters("Hello World!", 5, false, "[String = 'Hello World!']")]
    void NotIsHelloWorld(String input1, Int32 count, Boolean result, String message) {

        Statics.DDTResultState(() => DummyTest.IfNot.String.IsHelloWorld(input1),
            (count, result, message), "Test.IfNot.String.IsHelloWorld");

    } 

}