Skip to content

Latest commit

 

History

History
384 lines (316 loc) · 14.6 KB

combinations.md

File metadata and controls

384 lines (316 loc) · 14.6 KB

Combinations

Combinations allows all combinations of the given input lists to be executed, and the results all written to a single file.

Example

Method being tested

public static string BuildAddress(int streetNumber, string street, string city)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(street);
    ArgumentException.ThrowIfNullOrWhiteSpace(city);
    ArgumentOutOfRangeException.ThrowIfLessThan(streetNumber, 1);

    return $"{streetNumber} {street}, {city}";
}

snippet source | anchor

Test

[Fact]
public Task BuildAddressTest()
{
    int[] streetNumbers = [1, 10];
    string[] streets = ["Smith St", "Wallace St"];
    string[] cities = ["Sydney", "Chicago"];
    return Combination()
        .Verify(
            BuildAddress,
            streetNumbers,
            streets,
            cities);
}

snippet source | anchor

Result

{
   1, Smith St  , Sydney : 1 Smith St, Sydney,
   1, Smith St  , Chicago: 1 Smith St, Chicago,
   1, Wallace St, Sydney : 1 Wallace St, Sydney,
   1, Wallace St, Chicago: 1 Wallace St, Chicago,
  10, Smith St  , Sydney : 10 Smith St, Sydney,
  10, Smith St  , Chicago: 10 Smith St, Chicago,
  10, Wallace St, Sydney : 10 Wallace St, Sydney,
  10, Wallace St, Chicago: 10 Wallace St, Chicago
}

snippet source | anchor

Column Alignment

Key value are aligned based on type.

  • Numbers (int, double, float etc) are aligned right
  • All other types are aligned left

CaptureExceptions

By default exceptions are not captured. So if an exception is thrown by the method being tested, it will bubble up.

Exceptions can be optionally "captured". This approach uses the Exception.Message as the result of the method being tested.

To enable exception capture use captureExceptions = true:

[Fact]
public Task BuildAddressExceptionsTest()
{
    int[] streetNumbers = [-1, 0, 10];
    string[] streets = ["", " ", "Valid St"];
    string[] cities = [null!, "Valid City"];
    return Combination(captureExceptions: true)
        .Verify(
            BuildAddress,
            streetNumbers,
            streets,
            cities
        );
}

snippet source | anchor

Result

{
  -1,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  -1, Valid St, null      : ArgumentNullException: Value cannot be null. (Parameter 'city').,
  -1, Valid St, Valid City: ArgumentOutOfRangeException: streetNumber ('-1') must be greater than or equal to '1'. (Parameter 'streetNumber'). Actual value was -1.,
   0,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
   0, Valid St, null      : ArgumentNullException: Value cannot be null. (Parameter 'city').,
   0, Valid St, Valid City: ArgumentOutOfRangeException: streetNumber ('0') must be greater than or equal to '1'. (Parameter 'streetNumber'). Actual value was 0.,
  10,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10,         , null      : ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10,         , Valid City: ArgumentException: The value cannot be an empty string or composed entirely of whitespace. (Parameter 'street').,
  10, Valid St, null      : ArgumentNullException: Value cannot be null. (Parameter 'city').,
  10, Valid St, Valid City: 10 Valid St, Valid City
}

snippet source | anchor

Global CaptureExceptions

Exception capture can be enabled globally:

[ModuleInitializer]
public static void Initialize() =>
    CombinationSettings.CaptureExceptions();

snippet source | anchor

If exception capture has been enabled globally, it can be disable at the method test level using captureExceptions: false.

[Fact]
public Task BuildAddressExceptionsDisabledTest()
{
    int[] streetNumbers = [1, 10];
    string[] streets = ["Smith St", "Wallace St"];
    string[] cities = ["Sydney", "Chicago"];
    return Combination(captureExceptions: false)
        .Verify(
            BuildAddress,
            streetNumbers,
            streets,
            cities);
}

snippet source | anchor

Result serialization

Serialization of results is done using CombinationResultsConverter

namespace VerifyTests;

public class CombinationResultsConverter :
    WriteOnlyJsonConverter<CombinationResults>
{
    public override void Write(VerifyJsonWriter writer, CombinationResults results)
    {
        writer.WriteStartObject();

        var items = results.Items;
        if (items.Count == 0)
        {
            return;
        }

        var keysLength = items[0].Keys.Count;

        var maxKeyLengths = new int[keysLength];
        var keyValues = new string[items.Count, keysLength];

        for (var itemIndex = 0; itemIndex < items.Count; itemIndex++)
        {
            var item = items[itemIndex];
            for (var keyIndex = 0; keyIndex < keysLength; keyIndex++)
            {
                var key = item.Keys[keyIndex];
                var name = VerifierSettings.GetNameForParameter(key, pathFriendly: false);
                keyValues[itemIndex, keyIndex] = name;
                var currentKeyLength = maxKeyLengths[keyIndex];
                if (name.Length > currentKeyLength)
                {
                    maxKeyLengths[keyIndex] = name.Length;
                }
            }
        }

        var keys = new CombinationKey[keysLength];
        for (var itemIndex = 0; itemIndex < items.Count; itemIndex++)
        {
            for (var keyIndex = 0; keyIndex < keysLength; keyIndex++)
            {
                keys[keyIndex] = new(
                    Value: keyValues[itemIndex, keyIndex],
                    MaxLength: maxKeyLengths[keyIndex],
                    Type: results.KeyTypes?[keyIndex]);
            }

            var item = items[itemIndex];
            var name = BuildPropertyName(keys);
            writer.WritePropertyName(name);
            WriteValue(writer, item);
        }

        writer.WriteEndObject();
    }

    protected virtual string BuildPropertyName(IReadOnlyList<CombinationKey> keys)
    {
        var builder = new StringBuilder();
        foreach (var (value, maxLength, type) in keys)
        {
            var padding = maxLength - value.Length;
            if (type != null &&
                type.IsNumeric())
            {
                builder.Append(' ', padding);
                builder.Append(value);
            }
            else
            {
                builder.Append(value);
                builder.Append(' ', padding);
            }

            builder.Append(", ");
        }

        builder.Length -= 2;
        return builder.ToString();
    }

    protected virtual void WriteValue(VerifyJsonWriter writer, CombinationResult result)
    {
        switch (result.Type)
        {
            case CombinationResultType.Void:
                writer.WriteValue("void");
                break;
            case CombinationResultType.Value:
                if (result.Value == null)
                {
                    writer.WriteNull();
                }
                else
                {
                    writer.Serialize(result.Value);
                }
                break;
            case CombinationResultType.Exception:
                var exception = result.Exception;
                var message = exception.Message;
                if (exception is ArgumentException)
                {
                    message = FlattenMessage(message);
                }

                writer.WriteValue($"{exception.GetType().Name}: {message}");
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    static string FlattenMessage(string message)
    {
        var builder = new StringBuilder();

        foreach (var line in message.AsSpan().EnumerateLines())
        {
            var trimmed = line.TrimEnd();
            builder.Append(trimmed);
            if (!trimmed.EndsWith('.'))
            {
                builder.Append(". ");
            }
        }

        builder.TrimEnd();
        return builder.ToString();
    }
}

snippet source | anchor

Custom

Combination serialization can be customized using a Converter.

Converter

Inherit from CombinationResultsConverter and override the desired members.

The below sample override BuildPropertyName to customize the property name. It bypasses the default implementation and hence does not pad columns or use VerifierSettings.GetNameForParameter for key conversion.

class CustomCombinationConverter :
    CombinationResultsConverter
{
    protected override string BuildPropertyName(IReadOnlyList<CombinationKey> keys) =>
        string.Join(", ", keys.Select(_ => _.Value));
}

snippet source | anchor

Full control of serialization can be achieved by inheriting from WriteOnlyJsonConverter<CombinationResults>.

Insert Converter

Insert the new converter at the top of the converter stack.

static CustomCombinationConverter customConverter = new();

[ModuleInitializer]
public static void Init() =>
    VerifierSettings.AddExtraSettings(_ => _.Converters.Insert(0, customConverter));

snippet source | anchor

Result

{
  1, Smith St, Sydney: 1 Smith St, Sydney,
  1, Smith St, Chicago: 1 Smith St, Chicago,
  1, Wallace St, Sydney: 1 Wallace St, Sydney,
  1, Wallace St, Chicago: 1 Wallace St, Chicago,
  10, Smith St, Sydney: 10 Smith St, Sydney,
  10, Smith St, Chicago: 10 Smith St, Chicago,
  10, Wallace St, Sydney: 10 Wallace St, Sydney,
  10, Wallace St, Chicago: 10 Wallace St, Chicago
}

snippet source | anchor