Last active
May 6, 2020 12:08
-
-
Save svonidze/4477529162a138c101e3c022070e9fe3 to your computer and use it in GitHub Desktop.
SequenceAssertion to enhance the default not verbose InvaildOperationException thrown by Single/First methods
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Common.Types.Extensions; | |
public class SequenceAssertion | |
{ | |
public static class Messages | |
{ | |
public const string BeginningFormat = "Sequence of type \"{0}\" contains "; | |
public static class Elements | |
{ | |
public const string MoreThanOne = "more than one matching element"; | |
public const string NoOne = "no matching element"; | |
public const string Some = "some matching element but should not"; | |
} | |
} | |
} | |
public class SequenceAssertion<T> : SequenceAssertion, IDisposable | |
{ | |
public delegate Exception ExceptionFunc(string exceptionMessage); | |
private const int MoreThanOne = 2; | |
private const string SearchCriterionDelimiter = "; "; | |
private static readonly ExceptionFunc DefaultExceptionFunc = exceptionMessage => new InvalidOperationException(exceptio | |
private IEnumerable<T> sequence; | |
private ExceptionFunc ifAnyExceptionFunc; | |
private ExceptionFunc ifEmptyExceptionFunc; | |
private ExceptionFunc ifMoreThanOneExceptionFunc; | |
private Action<ExceptionFunc> setExceptionFunc; | |
private Func<string> searchCriteriaFunc; | |
private Func<string> dumpItemFunc; | |
private string searchCriteria; | |
private string typeNameOverride; | |
private bool isDisposed; | |
public void Dispose() | |
{ | |
if (this.isDisposed) | |
return; | |
this.isDisposed = true; | |
#if DEBUG | |
// System.Diagnostics.Debug.WriteLine($"{this.GetType().FullName} is being disposed"); | |
#endif | |
this.sequence = null; | |
this.ifAnyExceptionFunc = null; | |
this.ifEmptyExceptionFunc = null; | |
this.ifMoreThanOneExceptionFunc = null; | |
this.setExceptionFunc = null; | |
this.searchCriteriaFunc = null; | |
this.dumpItemFunc = null; | |
this.searchCriteria = null; | |
this.typeNameOverride = null; | |
} | |
internal SequenceAssertion(IEnumerable<T> sequence) | |
{ | |
this.sequence = sequence; | |
} | |
public SequenceAssertion<T> IfAny() | |
{ | |
this.setExceptionFunc = func => this.ifAnyExceptionFunc = func; | |
return this; | |
} | |
public SequenceAssertion<T> IfEmpty() | |
{ | |
this.setExceptionFunc = func => this.ifEmptyExceptionFunc = func; | |
return this; | |
} | |
public SequenceAssertion<T> IfMoreThanOne() | |
{ | |
this.setExceptionFunc = func => this.ifMoreThanOneExceptionFunc = func; | |
return this; | |
} | |
public SequenceAssertion<T> Throw(Func<Exception> exceptionFunc) | |
{ | |
this.setExceptionFunc(exceptionMessage => exceptionFunc()); | |
this.setExceptionFunc = null; | |
return this; | |
} | |
public SequenceAssertion<T> Throw(ExceptionFunc exceptionFunc) | |
{ | |
this.setExceptionFunc(exceptionFunc); | |
this.setExceptionFunc = null; | |
return this; | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="TException">must have ctor with one string arg for message</typeparam> | |
/// <returns></returns> | |
public SequenceAssertion<T> Throw<TException>(string exceptionMessage = default(string)) | |
where TException: Exception | |
{ | |
return this.Throw( | |
message => (TException)Activator.CreateInstance(typeof(TException), exceptionMessage ?? message)); | |
} | |
/// <summary> | |
/// | |
/// </summary> | |
/// <typeparam name="TException">must have ctor with one string arg for message</typeparam> | |
/// <returns></returns> | |
public SequenceAssertion<T> Throw<TException>(Func<string> exceptionMessage) | |
where TException: Exception | |
{ | |
return this.Throw(_ => (TException)Activator.CreateInstance(typeof(TException), exceptionMessage())); | |
} | |
/// <summary> | |
/// Warning! The method will walk over every item in the sequence | |
/// </summary> | |
/// <param name="getExceptionMessage"></param> | |
/// <returns></returns> | |
public SequenceAssertion<T> ThrowWithMessage(Func<IEnumerable<T>, string> getExceptionMessage) | |
{ | |
return this.Throw(_ => DefaultExceptionFunc(getExceptionMessage(this.sequence))); | |
} | |
public SequenceAssertion<T> ThrowWithMessage(Func<string> getExceptionMessage) | |
{ | |
return this.Throw(_ => DefaultExceptionFunc(getExceptionMessage())); | |
} | |
public SequenceAssertion<T> ThrowWithMessage(string exceptionMessage) | |
{ | |
return this.Throw(_ => DefaultExceptionFunc(exceptionMessage)); | |
} | |
public SequenceAssertion<T> Throw() | |
{ | |
return this.Throw(DefaultExceptionFunc); | |
} | |
public SequenceAssertion<T> OverrideSequenceType(string typeName) | |
{ | |
this.typeNameOverride = $"\"{typeName}\""; | |
return this; | |
} | |
public SequenceAssertion<T> OverrideSequenceType<TType>() | |
{ | |
this.typeNameOverride = typeof(TType).FullName; | |
return this; | |
} | |
public SequenceAssertion<T> WithSearchCriteria(string searchCriteriaValue) | |
{ | |
this.searchCriteria = searchCriteriaValue; | |
return this; | |
} | |
// object types should be representative like Enum or ITypeDef, values should be converted to string | |
public SequenceAssertion<T> WithSearchParams(params object[] args) | |
{ | |
this.searchCriteriaFunc = () => args.Select(a => $"{a.GetType().Name}=\"{a}\"").JoinToString(SearchCriterionDelimite | |
return this; | |
} | |
public SequenceAssertion<T> AddSearchParam(string key, object value) | |
{ | |
this.searchCriteria += $"{key}=\"{value}\"{SearchCriterionDelimiter}"; | |
return this; | |
} | |
/// <summary> | |
/// Warning! The method will walk over every item in the sequence | |
/// </summary> | |
/// <returns></returns> | |
public SequenceAssertion<T> AllowDump() | |
{ | |
return this.AllowDump(item => item.ToString()); | |
} | |
/// <summary> | |
/// Warning! The method will walk over every item in the sequence | |
/// </summary> | |
/// <returns></returns> | |
public SequenceAssertion<T> AllowDump(Func<string> dumpItems) | |
{ | |
this.dumpItemFunc = dumpItems; | |
return this; | |
} | |
/// <summary> | |
/// Warning! The method will walk over every item in the sequence | |
/// </summary> | |
/// <param name="dumpItems"></param> | |
/// <returns></returns> | |
public SequenceAssertion<T> AllowDump(Func<T, string> dumpItems) | |
{ | |
this.dumpItemFunc = () => this.sequence.Select(dumpItems).JoinToString(SearchCriterionDelimiter); | |
return this; | |
} | |
public SequenceAssertion<T> AllowSequenceDump(Func<IEnumerable<T>, string> dumpItems) | |
{ | |
this.dumpItemFunc = () => dumpItems(this.sequence); | |
return this; | |
} | |
public T Single(Func<T, bool> predicate = null) | |
{ | |
if (predicate != null) | |
this.sequence = this.sequence.Where(predicate); | |
return this.Get(Only.Single); | |
} | |
public T SingleOrDefault(Func<T, bool> predicate = null) | |
{ | |
if (predicate != null) | |
this.sequence = this.sequence.Where(predicate); | |
return this.Get(Only.Single | Only.Default); | |
} | |
public T First(Func<T, bool> predicate = null) | |
{ | |
if (predicate != null) | |
this.sequence = this.sequence.Where(predicate); | |
return this.Get(Only.First); | |
} | |
public T FirstOrDefault(Func<T, bool> predicate = null) | |
{ | |
if (predicate != null) | |
this.sequence = this.sequence.Where(predicate); | |
return this.Get(Only.First | Only.Default); | |
} | |
public IEnumerable<TResult> Select<TResult>(Func<T, TResult> selector) | |
{ | |
return this.AsEnumerable().Select(selector); | |
} | |
public IEnumerable<TResult> SelectMany<TResult>( | |
Func<T, IEnumerable<TResult>> selector) | |
{ | |
return this.AsEnumerable().SelectMany(selector); | |
} | |
public IEnumerable<T> AsEnumerable() | |
{ | |
try | |
{ | |
return this.sequence; | |
} | |
finally | |
{ | |
this.Dispose(); | |
} | |
} | |
public SequenceAssertion<T> Verify() | |
{ | |
this.Verify(this.sequence.Take(MoreThanOne).Count); | |
return this; | |
} | |
private void Verify(Func<int> getItemCount) | |
{ | |
var itemCount = getItemCount.InitLazy(); | |
ExceptionFunc exceptionFunc = null; | |
string message = null; | |
if (this.ifEmptyExceptionFunc != null && itemCount.Value == 0) | |
{ | |
message = Messages.Elements.NoOne; | |
exceptionFunc = this.ifEmptyExceptionFunc; | |
} | |
else if (this.ifMoreThanOneExceptionFunc != null && itemCount.Value > 1) | |
{ | |
message = Messages.Elements.MoreThanOne; | |
exceptionFunc = this.ifMoreThanOneExceptionFunc; | |
} | |
else if (this.ifAnyExceptionFunc != null && itemCount.Value > 0) | |
{ | |
message = Messages.Elements.Some; | |
exceptionFunc = this.ifAnyExceptionFunc; | |
} | |
if (exceptionFunc == null) | |
return; | |
message = string.Format(Messages.BeginningFormat, this.typeNameOverride ?? typeof(T).Name) + message; | |
this.searchCriteria = this.searchCriteria ?? this.searchCriteriaFunc?.Invoke(); | |
if (!string.IsNullOrWhiteSpace(this.searchCriteria)) | |
{ | |
message += $" with the search criteria {this.searchCriteria}"; | |
} | |
if (this.dumpItemFunc != null) | |
{ | |
message += ". Items: " + this.dumpItemFunc(); | |
} | |
try | |
{ | |
throw exceptionFunc(message); | |
} | |
finally | |
{ | |
this.Dispose(); | |
} | |
} | |
private T Get(Only only) | |
{ | |
var items = this.sequence.Take(MoreThanOne).ToList(); | |
switch (items.Count) | |
{ | |
case 1: | |
case MoreThanOne when only.HasFlag(Only.First): | |
var first = items.First(); | |
this.Dispose(); | |
return first; | |
case 0 when only.HasFlag(Only.Default): | |
this.Dispose(); | |
return default(T); | |
} | |
if (this.ifEmptyExceptionFunc == null) this.ifEmptyExceptionFunc = DefaultExceptionFunc; | |
if (this.ifMoreThanOneExceptionFunc == null) this.ifMoreThanOneExceptionFunc = DefaultExceptionFunc; | |
this.Verify(() => items.Count); | |
throw new NotSupportedException("Should not reach this code"); | |
} | |
[Flags] | |
private enum Only | |
{ | |
Undefined = 0, | |
Default = 1, | |
Single = 1 << 1, | |
First = 1 << 2 | |
} | |
} | |
public static class SequenceAssertionExtensions | |
{ | |
public static SequenceAssertion<T> AllowVerboseException<T>(this IEnumerable<T> enumerable) | |
{ | |
return new SequenceAssertion<T>(enumerable); | |
} | |
public static SequenceAssertion<T> IfEmpty<T>(this IEnumerable<T> enumerable) | |
{ | |
return enumerable.AllowVerboseException().IfEmpty(); | |
} | |
public static SequenceAssertion<T> IfMoreThanOne<T>(this IEnumerable<T> enumerable) | |
{ | |
return enumerable.AllowVerboseException().IfMoreThanOne(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Common.Types.Extensions; | |
using Common.UnitTesting; | |
using Xunit; | |
using Xunit.Extensions; | |
public class SequenceAssertionTests : TestSpecification | |
{ | |
[Fact] | |
public void Test_Enumeration() | |
{ | |
var sourceSequence = new [] {1, 2}; | |
var results = sourceSequence.AllowVerboseException().AsEnumerable().Where(x => x == 1); | |
Assert.Single(results); | |
Console.WriteLine(results.JoinToString(',')); | |
var sequenceAssertion = sourceSequence.AllowVerboseException(); | |
var results3 = sequenceAssertion.AsEnumerable(); | |
Assert.Equal(sourceSequence, results3); | |
Assert.Null(sequenceAssertion.AsEnumerable()); | |
var results1 = sourceSequence.AllowVerboseException().Select(x => x); | |
Assert.Equal(sourceSequence, results1); | |
} | |
[Theory] | |
[ClassData(typeof(TestCases))] | |
public void Test(TestCase<int> @case) | |
{ | |
this.ExecuteTestCase(@case, @case.Verify); | |
} | |
public class TestCase<T> : TestCaseItem | |
{ | |
private readonly SequenceAssertion<T> sequenceAssertion; | |
private Action<SequenceAssertion<T>> verification; | |
public TestCase(IEnumerable<T> inputSequence) | |
{ | |
this.sequenceAssertion = inputSequence.AllowVerboseException(); | |
} | |
public TestCase(SequenceAssertion<T> sequenceAssertion) | |
{ | |
this.sequenceAssertion = sequenceAssertion; | |
} | |
public TestCase<T> Assert(Action<SequenceAssertion<T>> assertion) | |
{ | |
assertion(this.sequenceAssertion); | |
return this; | |
} | |
public TestCase<T> SetVerification(Action<SequenceAssertion<T>> verification) | |
{ | |
this.verification = verification; | |
return this; | |
} | |
public void Verify() | |
{ | |
if (this.verification != null) | |
{ | |
this.verification.Invoke(this.sequenceAssertion); | |
} | |
else | |
{ | |
this.sequenceAssertion.Verify(); | |
} | |
this.sequenceAssertion.Dispose(); | |
} | |
} | |
private class TestCases : TestCaseDataProvider | |
{ | |
private static class MethodNames | |
{ | |
public const string IfEmpty = nameof(SequenceAssertion<int>.IfEmpty); | |
public const string IfAny = nameof(SequenceAssertion<int>.IfAny); | |
public const string IfMoreThanOne = nameof(SequenceAssertion<int>.IfMoreThanOne); | |
public const string Single = nameof(Enumerable.Single); | |
public const string SingleOrDefault = nameof(Enumerable.SingleOrDefault); | |
public const string First = nameof(Enumerable.First); | |
public const string FirstOrDefault = nameof(Enumerable.FirstOrDefault); | |
} | |
protected override IEnumerable<ITestCaseItem> GetTestCases() | |
{ | |
yield return new TestCase<int>(new[] { 1 }) | |
.Assert(assertion => assertion.IfEmpty().Throw().IfMoreThanOne().Throw()) | |
.SetName($"{MethodNames.IfEmpty} & {MethodNames.IfMoreThanOne} the Sequence has single element"); | |
yield return new TestCase<int>(new[] { 1 }) | |
.SetVerification(assertion => assertion.Single()) | |
.SetName($"{MethodNames.Single} the Sequence has single element"); | |
yield return new TestCase<int>(new[] { 1 }) | |
.SetVerification(assertion => assertion.SingleOrDefault()) | |
.SetName($"{MethodNames.SingleOrDefault} the Sequence has single element"); | |
yield return new TestCase<int>(new[] { 1 }) | |
.SetVerification(assertion => assertion.First()) | |
.SetName($"{MethodNames.First} the Sequence has single element"); | |
yield return new TestCase<int>(new[] { 1 }) | |
.SetVerification(assertion => assertion.FirstOrDefault()) | |
.SetName($"{MethodNames.FirstOrDefault} the Sequence has single element"); | |
// ================ | |
yield return new TestCase<int>(new int[0]) | |
.Assert(assertion => assertion.IfMoreThanOne().Throw()) | |
.SetName($"{MethodNames.IfMoreThanOne} the Sequence has no element"); | |
yield return new TestCase<int>(new int[0]) | |
.SetVerification(assertion => assertion.SingleOrDefault()) | |
.SetName($"{MethodNames.SingleOrDefault} the Sequence has no element"); | |
yield return new TestCase<int>(new int[0]) | |
.SetVerification(assertion => assertion.FirstOrDefault()) | |
.SetName($"{MethodNames.FirstOrDefault} the Sequence has no element"); | |
yield return new TestCase<int>(new int[0]) | |
.Assert(assertion => assertion.IfAny().Throw()) | |
.SetName($"{MethodNames.IfAny} the Sequence has no element"); | |
// ================ | |
yield return new TestCase<int>(new[] { 1, 2 }) | |
.Assert(assertion => assertion.IfEmpty().Throw()) | |
.SetName($"{MethodNames.IfEmpty} the Sequence has many elements"); | |
yield return new TestCase<int>(new[] { 1, 2 }) | |
.SetVerification(assertion => assertion.First()) | |
.SetName($"{MethodNames.First} the Sequence has many elements"); | |
yield return new TestCase<int>(new[] { 1, 2 }) | |
.SetVerification(assertion => assertion.FirstOrDefault()) | |
.SetName($"{MethodNames.FirstOrDefault} the Sequence has many elements"); | |
SequenceAssertion<int> Create(IEnumerable<int> sequence, Action<SequenceAssertion<int>> preset) | |
{ | |
var assertion = sequence.AllowVerboseException(); | |
preset(assertion); | |
return assertion; | |
} | |
var exceptionCases = new Dictionary<string, Func<SequenceAssertion<int>>> | |
{ | |
{ MethodNames.IfEmpty, () => Create(new int[0], a => a.IfEmpty()) }, | |
{ MethodNames.IfMoreThanOne, () => Create(new[] { 1, 2 }, a => a.IfMoreThanOne()) }, | |
{ MethodNames.IfAny, () => Create(new[] { 1 }, a => a.IfAny()) } | |
}; | |
foreach (var methodName in exceptionCases.Keys) | |
{ | |
var createAssertion = exceptionCases[methodName]; | |
yield return new TestCase<int>(createAssertion()) | |
.Assert(a => a.Throw()) | |
.Expect<InvalidOperationException>() | |
.SetName($"{methodName} Throw the default Exception"); | |
yield return new TestCase<int>(createAssertion()) | |
.Assert(a => a.ThrowWithMessage("Some custom message")) | |
.Expect<InvalidOperationException>("Some custom message") | |
.SetName($"{methodName} Throw the default Exception with a custom Message"); | |
yield return new TestCase<int>(createAssertion()) | |
.Assert(a => a.Throw<CustomException>()) | |
.Expect<CustomException>() | |
.SetName($"{methodName} Throw a custom Exception"); | |
yield return new TestCase<int>(createAssertion()) | |
.Assert(a => a.Throw<Exception>("Some custom message")) | |
.Expect<Exception>("Some custom message") | |
.SetName($"{methodName} Throw a custom Exception with a custom Message"); | |
} | |
} | |
} | |
private class CustomException : Exception | |
{ | |
public CustomException(string message) : base(message) | |
{ | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment