Skip to content

Instantly share code, notes, and snippets.

@einari
Created September 5, 2021 05:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save einari/dce8f2e787b96408c1e937da0d0900c4 to your computer and use it in GitHub Desktop.
Save einari/dce8f2e787b96408c1e937da0d0900c4 to your computer and use it in GitHub Desktop.
BDD - Specifications in xUnit
/// <summary>
/// Represents a wrapper for working with exceptions.
/// </summary>
public static class Catch
{
/// <summary>
/// Catch any exception that occurs from the wrapped callback.
/// </summary>
/// <param name="callback">Callback to wrap.</param>
/// <returns>Exception that happened - if any. Null if not.</returns>
public static Exception? Exception(Action callback)
{
try
{
callback();
}
catch (Exception ex)
{
return ex;
}
return null;
}
/// <summary>
/// Catch a specific exception that occurs from the wrapped callback.
/// </summary>
/// <typeparam name="T">Type of exception to catch.</typeparam>
/// <param name="callback">Callback to wrap.</param>
/// <returns>Exception that happened - if any. Null if not.</returns>
public static T? Exception<T>(Action callback)
where T:Exception
{
try
{
callback();
}
catch (T ex)
{
return ex;
}
return null;
}
}
/// <summary>
/// Holds extension methods for fluent "Should*" assertions related to collections.
/// </summary>
public static class ShouldCollectionExtensions
{
/// <summary>
/// Assert that a collection only contains the expected elements.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected values.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldContainOnly<T>(this IEnumerable<T> collection, IEnumerable<T> expected)
{
var source = new List<T>(collection);
var noContain = new List<T>();
foreach (var item in expected)
{
if (!source.Contains<T>(item))
{
noContain.Add(item);
}
else
{
source.Remove(item);
}
}
if( noContain.Count > 0 || source.Count > 0)
{
var sourceItems = string.Join(",", collection);
var expectedItems = string.Join(",", expected);
Assert.True(false, $"Collection '{sourceItems}' does not only contain '{expectedItems}'");
}
}
/// <summary>
/// Assert that a collection only contains the expected elements - based on params.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected values.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldContainOnly<T>(this IEnumerable<T> collection, params T[] expected)
{
collection.ShouldContainOnly(expected as IEnumerable<T>);
}
/// <summary>
/// Assert that a collection contains exactly only the expected elements in the same sequence.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected values.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldEqual<T>(this IEnumerable<T> collection, IEnumerable<T> expected)
{
Assert.True(collection.SequenceEqual(expected), $"Collection '{collection}' is not equal to '{expected}'");
}
/// <summary>
/// Assert that a collection contains exactly only the expected elements in the same sequence - based on params.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected values.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldEqual<T>(this IEnumerable<T> collection, params T[] expected)
{
collection.ShouldEqual(expected as IEnumerable<T>);
}
/// <summary>
/// Assert that a collection contains all the expected elements.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected elements.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldContain<T>(this IEnumerable<T> collection, IEnumerable<T> expected)
{
foreach (var item in expected)
{
Assert.Contains(item, collection);
}
}
/// <summary>
/// Assert that a collection contains all the expected elements - based on params.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected elements as params.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldContain<T>(this IEnumerable<T> collection, params T[] expected)
{
collection.ShouldContain(expected as IEnumerable<T>);
}
/// <summary>
/// Assert that a dictionary contains a specific key.
/// </summary>
/// <param name="actual">Dictionary to assert.</param>
/// <param name="expected">Expected key.</param>
/// <typeparam name="TKey">Type of key.</typeparam>
/// <typeparam name="TValue">Type of value.</typeparam>
public static void ShouldContain<TKey, TValue>(this IDictionary<TKey, TValue> actual, TKey expected)
{
Assert.Contains(expected, actual);
}
/// <summary>
/// Assert that a collection contains specific element(s) based on a predicate filter.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="filter">Filter to apply.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldContain<T>(this IEnumerable<T> collection, Predicate<T> filter)
{
Assert.Contains(collection, filter);
}
/// <summary>
/// Assert that a collection contains a specific element.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected element.</param>
/// <typeparam name="T">Type of element</typeparam>
public static void ShouldContain<T>(this IEnumerable<T> collection, T expected)
{
Assert.Contains(expected, collection);
}
/// <summary>
/// Assert that a dictionary does not contain a specific key.
/// </summary>
/// <param name="actual">Dictionary to assert.</param>
/// <param name="expected">Not expected key.</param>
/// <typeparam name="TKey">Type of key.</typeparam>
/// <typeparam name="TValue">Type of value.</typeparam>
public static void ShouldNotContain<TKey, TValue>(this IDictionary<TKey, TValue> actual, TKey expected)
{
Assert.DoesNotContain(expected, actual);
}
/// <summary>
/// Assert that a collection does not contain specific element(s) based on a predicate filter.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="filter">Filter to apply.</param>
/// <typeparam name="T">Type of element.</typeparam>
public static void ShouldNotContain<T>(this IEnumerable<T> collection, Predicate<T> filter)
{
Assert.DoesNotContain(collection, filter);
}
/// <summary>
/// Assert that a collection does not contain a specific element.
/// </summary>
/// <param name="collection">Collection to assert.</param>
/// <param name="expected">Expected element.</param>
/// <typeparam name="T">Type of element</typeparam>
public static void ShouldNotContain<T>(this IEnumerable<T> collection, T expected)
{
Assert.DoesNotContain(expected, collection);
}
/// <summary>
/// Assert that a collection is empty.
/// </summary>
/// <param name="collection">Collection to assert.</param>
public static void ShouldBeEmpty(this IEnumerable collection)
{
Assert.Empty(collection);
}
/// <summary>
/// Assert that a collection is not empty.
/// </summary>
/// <param name="collection">Collection to assert.</param>
public static void ShouldNotBeEmpty(this IEnumerable collection)
{
Assert.NotEmpty(collection);
}
/// <summary>
/// Assert that a collection has a single item.
/// </summary>
/// <param name="collection">Collection to assert.</param>
public static void ShouldContainSingleItem(this IEnumerable collection)
{
Assert.Single(collection);
}
}
/// <summary>
/// Holds extension methods for fluent "Should*" assertions related to comparables.
/// </summary>
public static class ShouldComparableExtensions
{
/// <summary>
/// Assert that a value is within range.
/// </summary>
/// <param name="actual">Value to compare.</param>
/// <param name="low">Lowest value in range.</param>
/// <param name="high">Highest value in range.</param>
/// <typeparam name="T">Type to compare.</typeparam>
public static void ShouldBeInRange<T>(this T actual, T low, T high)
where T : IComparable
{
Assert.InRange(actual, low, high);
}
/// <summary>
/// Assert that a value is not within range.
/// </summary>
/// <param name="actual">Value to compare.</param>
/// <param name="low">Lowest value in range.</param>
/// <param name="high">Highest value in range.</param>
/// <typeparam name="T">Type to compare.</typeparam>
public static void ShouldNotBeInRange<T>(this T actual, T low, T high)
where T : IComparable
{
Assert.NotInRange(actual, low, high);
}
/// <summary>
/// Assert that a value is greater than the other.
/// </summary>
/// <param name="left">Left value.</param>
/// <param name="right">Right value.</param>
public static void ShouldBeGreaterThan(this IComparable left, IComparable right)
{
Assert.True(left.CompareTo(right) > 0, $"{left} should be greater than {right}");
}
/// <summary>
/// Assert that a value is greater or equal than the other.
/// </summary>
/// <param name="left">Left value.</param>
/// <param name="right">Right value.</param>
public static void ShouldBeGreaterThanOrEqual(this IComparable left, IComparable right)
{
Assert.True(left.CompareTo(right) >= 0, $"{left} should be greater than or equal to {right}");
}
/// <summary>
/// Assert that a value is less than the other.
/// </summary>
/// <param name="left">Left value.</param>
/// <param name="right">Right value.</param>
public static void ShouldBeLessThan(this IComparable left, IComparable right)
{
Assert.True(left.CompareTo(right) < 0, $"{left} should be less than {right}");
}
/// <summary>
/// Assert that a value is less than or equal than the other.
/// </summary>
/// <param name="left">Left value.</param>
/// <param name="right">Right value.</param>
public static void ShouldBeLessThanOrEqual(this IComparable left, IComparable right)
{
Assert.True(left.CompareTo(right) <= 0, $"{left} should be less than or equal to {right}");
}
}
/// <summary>
/// Holds extension methods for fluent "Should*" assertions related to equality checks.
/// </summary>
public static class ShouldEqualityExtensions
{
/// <summary>
/// Assert that an object is null.
/// </summary>
/// <param name="actual">Actual value.</param>
public static void ShouldBeNull(this object actual)
{
Assert.Null(actual);
}
/// <summary>
/// Assert that an object is not null.
/// </summary>
/// <param name="actual">Actual value.</param>
public static void ShouldNotBeNull(this object actual)
{
Assert.NotNull(actual);
}
/// <summary>
/// Assert that a boolean is false.
/// </summary>
/// <param name="actual">Actual value.</param>
public static void ShouldBeFalse(this bool actual)
{
Assert.False(actual);
}
/// <summary>
/// Assert that a boolean is true.
/// </summary>
/// <param name="actual">Actual value.</param>
public static void ShouldBeTrue(this bool actual)
{
Assert.True(actual);
}
/// <summary>
/// Assert that two objects are equal.
/// </summary>
/// <param name="actual">Actual value.</param>
/// <param name="expected">Expected value.</param>
/// <typeparam name="T">Type of object.</typeparam>
public static void ShouldEqual<T>(this T actual, T expected)
{
Assert.Equal(expected, actual);
}
/// <summary>
/// Assert that two objects are not equal.
/// </summary>
/// <param name="actual">Actual value.</param>
/// <param name="expected">Expected value.</param>
/// <typeparam name="T">Type of object.</typeparam>
public static void ShouldNotEqual<T>(this T actual, T expected)
{
Assert.NotEqual(expected, actual);
}
/// <summary>
/// Assert that two objects are the same.
/// </summary>
/// <param name="actual">Actual value.</param>
/// <param name="expected">Expected value.</param>
public static void ShouldBeSame(this object actual, object expected)
{
Assert.Same(expected, actual);
}
/// <summary>
/// Assert that two objects are not the same.
/// </summary>
/// <param name="actual">Actual value.</param>
/// <param name="expected">Expected value.</param>
public static void ShouldNotBeSame(this object actual, object expected)
{
Assert.NotSame(expected, actual);
}
/// <summary>
/// Assert that two objects are not similar - a non strict equal. <see cref="Assert.NotStrictEqual{T}(T, T)"/>.
/// </summary>
/// <param name="actual">Actual value.</param>
/// <param name="expected">Expected value.</param>
/// <typeparam name="T">Type of object.</typeparam>
public static void ShouldNotBeSimilar<T>(this T actual, T expected)
{
Assert.NotStrictEqual(expected, actual);
}
}
/// <summary>
/// Holds extension methods for fluent "Should*" assertions related to strings.
/// </summary>
public static class ShouldStringExtensions
{
/// <summary>
/// Assert that a string contains an expected substring.
/// </summary>
/// <param name="actual">Actual string to assert.</param>
/// <param name="expectedSubstring">Expected substring.</param>
/// <param name="comparisonType">Optional <see cref="StringComparison">comparison type</see></param>
public static void ShouldContain(this string actual, string expectedSubstring, StringComparison comparisonType = StringComparison.CurrentCulture)
{
Assert.Contains(expectedSubstring, actual, comparisonType);
}
/// <summary>
/// Assert that a string does not contain an expected substring.
/// </summary>
/// <param name="actual">Actual string to assert.</param>
/// <param name="expectedSubstring">Not expected substring.</param>
/// <param name="comparisonType">Optional <see cref="StringComparison">comparison type</see></param>
public static void ShouldNotContain(this string actual, string expectedSubstring, StringComparison comparisonType = StringComparison.CurrentCulture)
{
Assert.DoesNotContain(expectedSubstring, actual, comparisonType);
}
}
/// <summary>
/// Holds extension methods for fluent "Should*" assertions related to types.
/// </summary>
public static class ShouldTypeExtensions
{
/// <summary>
/// Asserts that an object is assignable from a specific type.
/// </summary>
/// <param name="actual">Object to assert.</param>
/// <typeparam name="T">Type it should be assignable from.</typeparam>
public static void ShouldBeAssignableFrom<T>(this object actual)
{
Assert.IsAssignableFrom<T>(actual);
}
/// <summary>
/// Asserts that an object is assignable from a specific type.
/// </summary>
/// <param name="actual">Object to assert.</param>
/// <param name="expected">Type it should be assignable from.</param>
public static void ShouldBeAssignableFrom(this object actual, Type expected)
{
Assert.IsAssignableFrom(expected, actual);
}
/// <summary>
/// Assert that an object is of an exact type.
/// </summary>
/// <param name="actual">Object to assert.</param>
/// <typeparam name="T">Type it should be.</typeparam>
public static void ShouldBeOfExactType<T>(this object actual)
{
Assert.IsType<T>(actual);
}
}
/// <summary>
/// Represents the base class for specifications.
/// </summary>
/// <remarks>
/// The lifecycle of a specifiction is as follows:
/// - Establish : establishes the context (Given)
/// - Because : performs the action (When)
/// - [Fact] : all your facts (Then)
/// - Destroy : performs cleanup of the context
/// .
/// The different methods are by convention, meaning that having a private method called "Establish", "Destroy", "Because"
/// with a void signature taking no arguments will automatically be called.
/// All "Then" statements are considered the xUnit Facts we want to run assertions for.
/// .
/// It will recursively execute lifecycle methods in the inheritance hierarchy.
/// This enables one to encapsulate reusable contexts. The order it executes them in
/// is reversed; meaning that it will start at the lowest level in the inheritance chain
/// and move towards the specific type.
/// Example:
/// Context class:
/// public class a_specific_context : Specification
/// {
/// void Establish() => ....
/// }
/// .
/// Specification class:
/// _
/// public class when_doing_something : a_specific_context
/// {
/// void Establish() => ....
/// void Because() => ....
/// [Fact] It_should_do_something() => ....
/// }
/// .
/// It will run the Establish first for the `a_specific_context` and then the `when_doing_something`
/// class.
/// </remarks>
public class Specification : IDisposable
{
/// <summary>
/// Initializes a new instance of <see cref="Specification"/>.
/// </summary>
public Specification()
{
OnEstablish();
OnBecause();
}
/// <inheritdoc/>
public void Dispose()
{
OnDestroy();
GC.SuppressFinalize(this);
}
void OnEstablish()
{
InvokeMethod("Establish");
}
void OnBecause()
{
InvokeMethod("Because");
}
void OnDestroy()
{
InvokeMethod("Destroy");
}
void InvokeMethod(string name)
{
typeof(SpecificationMethods<>)
.MakeGenericType(GetType())
.GetMethod(name, BindingFlags.Static | BindingFlags.Public)?
.Invoke(null, new object[] { this });
}
}
/// <summary>
/// Represents the lifecycle methods for a <see cref="Specification"/>.
/// </summary>
/// <typeparam name="T">Target type it represents.</typeparam>
/// <remarks>
/// It will recursively execute lifecycle methods in the inheritance hierarchy.
/// This enables one to encapsulate reusable contexts. The order it executes them in
/// is reversed; meaning that it will start at the lowest level in the inheritance chain
/// and move towards the specific type.
/// Example:
/// Context class:
/// public class a_specific_context : Specification
/// {
/// void Establish() => ....
/// }
/// _
/// Specification class:
/// _
/// public class when_doing_something : a_specific_context
/// {
/// void Establish() => ....
/// }
/// -
/// It will run the Establish first for the `a_specific_context` and then the `when_doing_something`
/// class.
/// </remarks>
public static class SpecificationMethods<T>
{
static IEnumerable<MethodInfo> _establish { get; }
static IEnumerable<MethodInfo> _because { get; }
static IEnumerable<MethodInfo> _destroy { get; }
static SpecificationMethods()
{
_establish = GetMethodsFor("Establish");
_destroy = GetMethodsFor("Destroy");
_because = GetMethodsFor("Because");
}
static IEnumerable<MethodInfo> GetMethodsFor(string name)
{
var type = typeof(T);
var methods = new List<MethodInfo>();
while (type != typeof(Specification))
{
var method = type!.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (method != null) methods.Insert(0, method);
type = type.BaseType;
}
return methods;
}
/// <summary>
/// Invoke all Establish methods.
/// </summary>
/// <param name="unit">Unit to invoke them on.</param>
public static void Establish(object unit) => InvokeMethods(_establish, unit);
/// <summary>
/// Invoke all Destroy methods.
/// </summary>
/// <param name="unit">Unit to invoke them on.</param>
public static void Destroy(object unit) => InvokeMethods(_destroy, unit);
/// <summary>
/// Invoke all Because methods.
/// </summary>
/// <param name="unit">Unit to invoke them on.</param>
public static void Because(object unit) => InvokeMethods(_because, unit);
static void InvokeMethods(IEnumerable<MethodInfo> methods, object unit)
{
foreach (var method in methods) method.Invoke(unit, Array.Empty<object>());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment