# 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 ```cs public static string BuildAddress(int number, string street, string city) { ArgumentException.ThrowIfNullOrWhiteSpace(street); ArgumentException.ThrowIfNullOrWhiteSpace(city); ArgumentOutOfRangeException.ThrowIfLessThan(number, 1); return $"{number} {street}, {city}"; } ``` snippet source | anchor ### Test ```cs [Fact] public Task BuildAddressTest() { int[] number = [1, 10]; string[] street = ["Smith St", "Wallace St"]; string[] city = ["Sydney", "Chicago"]; return Combination() .Verify( BuildAddress, number, street, city); } ``` snippet source | anchor ### Result ```txt { 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`: ```cs [Fact] public Task BuildAddressExceptionsTest() { int[] number = [-1, 0, 10]; string[] street = ["", " ", "Valid St"]; string[] city = [null!, "Valid City"]; return Combination(captureExceptions: true) .Verify( BuildAddress, number, street, city ); } ``` snippet source | anchor ### Result ```txt { -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: number ('-1') must be greater than or equal to '1'. (Parameter 'number'). 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: number ('0') must be greater than or equal to '1'. (Parameter 'number'). 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: ```cs [ModuleInitializer] public static void EnableCaptureExceptions() => CombinationSettings.CaptureExceptions(); ``` snippet source | anchor If exception capture has been enabled globally, it can be disable at the method test level using `captureExceptions: false`. ```cs [Fact] public Task BuildAddressExceptionsDisabledTest() { int[] number = [1, 10]; string[] street = ["Smith St", "Wallace St"]; string[] city = ["Sydney", "Chicago"]; return Combination(captureExceptions: false) .Verify( BuildAddress, number, street, city); } ``` snippet source | anchor ## Result serialization Serialization of results is done using `CombinationResultsConverter` ```cs namespace VerifyTests; public class CombinationResultsConverter : WriteOnlyJsonConverter { 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; int[] maxKeyLengths; if (results.Columns == null) { maxKeyLengths = new int[keysLength]; } else { maxKeyLengths = results.Columns.Select(_=>_.Length).ToArray(); } 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, writer.Counter, pathFriendly: false); keyValues[itemIndex, keyIndex] = name; var currentKeyLength = maxKeyLengths[keyIndex]; if (name.Length > currentKeyLength) { maxKeyLengths[keyIndex] = name.Length; } } } WriteColumns(writer, results, maxKeyLengths); // keys is reused 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(); } static void WriteColumns(VerifyJsonWriter writer, CombinationResults results, int[] maxKeyLengths) { if (results.Columns == null) { return; } var builder = new StringBuilder(); for (var index = 0; index < results.Columns.Count; index++) { var column = results.Columns[index]; var maxLength = maxKeyLengths[index]; var padding = maxLength - column.Length; builder.Append(column); builder.Append(' ', padding); builder.Append(", "); } builder.Length -= 2; writer.WritePropertyName(builder.ToString()); writer.WriteValue("Result"); } protected virtual string BuildPropertyName(IReadOnlyList 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. ```cs class CustomCombinationConverter : CombinationResultsConverter { protected override string BuildPropertyName(IReadOnlyList keys) => string.Join(", ", keys.Select(_ => _.Value)); } ``` snippet source | anchor Full control of serialization can be achieved by inheriting from `WriteOnlyJsonConverter`. #### Insert Converter Insert the new converter at the top of the converter stack. ```cs static CustomCombinationConverter customConverter = new(); [ModuleInitializer] public static void Init() => VerifierSettings.AddExtraSettings(_ => _.Converters.Insert(0, customConverter)); ``` snippet source | anchor #### Result ```txt { streetNumbers, streets , cities : 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 ## Header By default no column headers are included. To include a header pass through `header: true` ```cs [Fact] public Task BuildAddressWithHeaderTest() { int[] number = [1, 10]; string[] street = ["Smith St", "Wallace St"]; string[] city = ["Sydney", "Chicago"]; return Combination(header: true) .Verify( BuildAddress, number, street, city); } ``` snippet source | anchor The variable names of the inputted collections will be used. Result: ```txt { number, street , city : 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 ### Override Header names can be overridden: ```cs [Fact] public Task BuildAddressWithHeaderOverridesTest() { int[] number = [1, 10]; string[] street = ["Smith St", "Wallace St"]; string[] city = ["Sydney", "Chicago"]; return Combination(header: true) .Verify( BuildAddress, number, street, city, "Number", "Street", "City"); } ``` snippet source | anchor Result: ```txt { Number, Street , City : 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 ### Global Headers can be enabled globally: ```cs [ModuleInitializer] public static void EnableIncludeHeaders() => CombinationSettings.IncludeHeaders(); ``` snippet source | anchor