Guide: Writing tests

This article uses types from the samples to demonstrate, how efficient tests can be written when using the test instructions NuGet package for Nuclear.Test. All required types are available through the namespace Nuclear.TestSite, so a using directive must be added.

Decorating a test class as such is not required, unless a non-default behaviour is desired for any of the declared test methods. However, a test class may be given the TestClassAttribute, if all contained test methods should be ignored or invoked without parallelization. This can be achieved by setting the parameters ignoreReason or testMode during construction of the attribute.

All test methods of an assembly are invoked in parallel by default. All test methods that are configured for sequential invocation will be invoked first, before the rest follows suit using the default parallel behaviour. A test method can be ignored or invoked without parallelization. This can be achieved by setting the parameters ignoreReason or testMode during construction of the test method attribute. Every invocation of a test method uses a new instance of the test class type, which is disposed in the end.

Test instructions are organized in specialized test suites and are accessed through the static class Test, similar to assertions in traditional test frameworks. Every call of a test instruction can be negated through the use of the appropriate accessor (Test.If and Test.IfNot), so that there is no need for negated method implementation. There are currently nine individual test suites with a combined total of 286 test instructions. Test instructions will never trigger an exceptional abort, so that all instructions will always be evaluated.

Test suiteInstructionsIncl. overloadsIncl. negations
ActionTestSuite3510
TypeTestSuite248
ObjectTestSuite3510
StringTestSuite51224
ValueTestSuite94794
EnumerableTestSuite84590
ReferenceTestSuite112
FileTestSuite3612
DirectoryTestSuite51836

Ordinary test methods

Test methods must be decorated with the TestMethodAttribute, similar to a traditional test framework. The difference is that an access modifier is not required anywhere. Since a test method will test a code unit for the presence or absence of exceptional behaviour, almost all test methods will begin with one test instruction from the ActionTestSuite. Additional instructions test the validity of the object’s state after the tested action has finished. There is no upper limit on the number of test instructions in a single test method.

// Person_uTests.cs
class Person_uTests {

    [TestMethod]
    void CtorThrowsOnNull() {

        Test.If.Action.ThrowsException(() => new Person(null, null), out ArgumentNullException ex);
        Test.If.Value.IsEqual(ex.ParamName, "firstName");

    }

}

Parameterized test methods

To make test code reusable, a test method can have any number of parameters feeding data into the instructions. The parameter data for the invocation can be provided by using two different attributes.

The TestParametersAttribute is used to provide the necessary data to invoke a test method once. This means that a test method with two test instructions and three test parameter attributes generates a total of six test results during execution.

If the parameter types don’t permit the usage of the test parameters attribute or if the data is created at runtime, then the TestDataAttribute can be used instead. The attribute uses a type and a member name to resolve anything that can act as a data provider. A data provider can be any field, property or method of the test class or any other class. Another possible provider is a type that implements IEnumerable<Object[]> and that can be instantiated.

// Person_uTests.cs
class Person_uTests {

    [TestMethod]
    [TestParameters(null, null, "firstName")]
    [TestParameters("John", null, "lastName")]
    [TestParameters(null, "Doe", "firstName")]
    [TestData(nameof(CtorThrowsData))]
    void CtorThrows(String firstName, String lastName, String paramName) {

        Test.If.Action.ThrowsException(() => new Person(firstName, lastName), out Exception ex);
        Test.If.Value.IsEqual(ex.ParamName, paramName);

    }

    IEnumerable<Object> CtorThrowsData() {
        return new List<Object[]> {
            new Object[] { null, null, "firstName" },
            new Object[] { "John", null, "firstName" },
            new Object[] { null, "Doe", "firstName" }
        };
    }

}

Generic test methods

Supporting parameterized test methods is already pretty good, but we can do a lot better still. Full support of generic test methods is the holy grail, since it can improve the efficiency of testing by quite a bit. Usage isn’t that much different to non-generic test methods. Every generic type parameter requires a type to be provided before the actual parameter data.

// Calculator_uTests.cs
class Calculator_uTests {

    [TestMethod]
    [TestData(nameof(AddData))]
    void Add<T>(ICalculator<T> calculator, T lhs, T rhs, T expected) {

        T result = default;

        Test.IfNot.Action.ThrowsException(() => result = calculator.Add(lhs, rhs), out Exception ex);
        Test.If.Value.IsEqual(result, expected);

    }

    IEnumerable<Object[]> AddData() {
        return new List<Object[]>() {       
            new Object[] { typeof(Int32), new CalculatorInt32(), 0, 0, 0 },
            new Object[] { typeof(Int32), new CalculatorInt32(), 0, 2, 2 },
            // ...

            new Object[] { typeof(Int64), new CalculatorInt64(), (Int64) 0, (Int64) 0, (Int64) 0 },
            new Object[] { typeof(Int64), new CalculatorInt64(), (Int64) 0, (Int64) 2, (Int64) 2 },
            // ...

            new Object[] { typeof(Single), new CalculatorSingle(), (Single) 0, (Single) 0, (Single) 0 },
            new Object[] { typeof(Single), new CalculatorSingle(), (Single) 0, (Single) 2, (Single) 2 },
            // ...

            new Object[] { typeof(Double), new CalculatorDouble(), (Double) 0, (Double) 0, (Double) 0 },
            new Object[] { typeof(Double), new CalculatorDouble(), (Double) 0, (Double) 2, (Double) 2 },
            // ...

            new Object[] { typeof(Decimal), new CalculatorDecimal(), (Decimal) 0, (Decimal) 0, (Decimal) 0 },
            new Object[] { typeof(Decimal), new CalculatorDecimal(), (Decimal) 0, (Decimal) 2, (Decimal) 2 },
            // ...

        };
    }

}