Created
September 5, 2021 05:55
-
-
Save einari/dce8f2e787b96408c1e937da0d0900c4 to your computer and use it in GitHub Desktop.
BDD - Specifications in xUnit
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
/// <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; | |
} | |
} |
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
/// <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); | |
} | |
} |
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
/// <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}"); | |
} | |
} |
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
/// <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); | |
} | |
} |
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
/// <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); | |
} | |
} |
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
/// <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); | |
} | |
} |
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
/// <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 }); | |
} | |
} |
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
/// <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