Skip to content

Instantly share code, notes, and snippets.

@svonidze
Last active May 6, 2020 12:08
Show Gist options
  • Save svonidze/4477529162a138c101e3c022070e9fe3 to your computer and use it in GitHub Desktop.
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
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();
}
}
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