xUnit Theories with Type Parameters

Published on Wednesday, July 1, 2020

When unit testing generic protocols like the Lego Wireless Protocol input values might be lead to different expected values. Sometimes, however, not only the value changes but also the data types of what to expect. xUnit's Assert.Equal(expected, actual) methods has overloads for countless basic data types like short, double, bool and elementary support for enums. To activate these, the expected argument would need to be parameterized with the right data type.

While this works quite nice when using fixed unit tests where the actual/expected data type is part of the method, for xUnit theories it need some more infrastructure in form of adding a type parameter to the unit test method.

Here an extract from the unit testing from the sharpbrick/powered-up library.

[Theory]
[InlineData("06-00-01-02-06-00", HubProperty.Button, HubPropertyOperation.Update, false)]
[InlineData("06-00-01-02-06-01", HubProperty.Button, HubPropertyOperation.Update, true)]
[InlineData("06-00-01-05-06-61", HubProperty.Rssi, HubPropertyOperation.Update, (sbyte)97)]
[InlineData("06-00-01-06-06-64", HubProperty.BatteryVoltage, HubPropertyOperation.Update, (byte)100)]
[InlineData("06-00-01-07-06-00", HubProperty.BatteryType, HubPropertyOperation.Update, BatteryType.Normal)]
[InlineData("06-00-01-07-06-01", HubProperty.BatteryType, HubPropertyOperation.Update, BatteryType.RechargeableBlock)]
[InlineData("06-00-01-0B-06-80", HubProperty.SystemTypeId, HubPropertyOperation.Update, SystemType.LegoTechnic_MediumHub)]
[InlineData("06-00-01-0C-06-00", HubProperty.HardwareNetworkId, HubPropertyOperation.Update, (byte)0)]
public void HubPropertiesEncoder_Decode_UpdateUpstream<T>(string messageAsString, HubProperty expectedProperty, HubPropertyOperation expectedPropertyOperation, T payload)
{
    // arrange
    var data = BytesStringUtil.StringToData(messageAsString).AsSpan().Slice(3);

    // act
    var message = new HubPropertiesEncoder().Decode(0x00, data) as HubPropertyMessage<T>;

    // assert
    Assert.Equal(expectedProperty, message.Property);
    Assert.Equal(expectedPropertyOperation, message.Operation);
    Assert.Equal(payload, message.Payload);
}

The payload parameter has a data type bound to a generic. When invoking the [Theory] xUnit will actually instanciate the function with the suitable type parameter. As above, sometimes the [InlineData(...)] entry needs to be hinted (e.g. (sbyte)97), when C# defaults might lead to a wrong data type (e.g. 97 is of data type int).

When the Attributes of C# do not allow to instantiate a data type (typically classes), a adapter shim might help:

[Theory]
[InlineData("09-00-01-03-06-00-00-00-11", HubProperty.FwVersion, HubPropertyOperation.Update, "1.1.0.0")]
[InlineData("09-00-01-04-06-00-00-00-07", HubProperty.HwVersion, HubPropertyOperation.Update, "0.7.0.0")]
[InlineData("07-00-01-0A-06-00-03", HubProperty.LegoWirelessProtocolVersion, HubPropertyOperation.Update, "3.0")]
public void HubPropertiesEncoder_Decode_UpdateUpstream_VersionShim(string messageAsString, HubProperty expectedProperty, HubPropertyOperation expectedPropertyOperation, string payload)
    => HubPropertiesEncoder_Decode_UpdateUpstream(messageAsString, expectedProperty, expectedPropertyOperation, new Version(payload));

Disclaimer: I google. This feature was surely somewhere mentioned before. I do not reference the material I googled, because I lost the link.