using System.Collections;
using System.Diagnostics.CodeAnalysis;
namespace XFEExtension.NetCore.XUnit.Assertions;
///
/// 提供不依赖全局状态的强类型测试断言;断言失败时抛出 。
///
public static class Assert
{
///
/// 验证条件为 。
///
/// 要验证的布尔条件。
/// 断言失败时使用的可选消息。
/// 为 。
public static void True([DoesNotReturnIf(false)] bool condition, string? message = null)
{
if (!condition)
throw new XFEAssertionException(message ?? "Expected true, but found false.");
}
///
/// 验证条件为 。
///
/// 要验证的布尔条件。
/// 断言失败时使用的可选消息。
/// 为 。
public static void False([DoesNotReturnIf(true)] bool condition, string? message = null)
{
if (condition)
throw new XFEAssertionException(message ?? "Expected false, but found true.");
}
///
/// 验证期望值与实际值相等;对于非字符串可枚举值,会按顺序逐项比较。
///
/// 参与比较的值类型。
/// 期望值。
/// 实际值。
/// 断言失败时使用的可选消息。
/// 两个值不相等,或两个序列的长度或元素不同。
public static void Equal(T expected, T actual, string? message = null)
{
if (expected is IEnumerable expectedItems && actual is IEnumerable actualItems && expected is not string && actual is not string)
{
var expectedEnumerator = expectedItems.GetEnumerator();
var actualEnumerator = actualItems.GetEnumerator();
var index = 0;
try
{
while (true)
{
var hasExpected = expectedEnumerator.MoveNext();
var hasActual = actualEnumerator.MoveNext();
if (!hasExpected && !hasActual)
return;
if (hasExpected != hasActual || !object.Equals(expectedEnumerator.Current, actualEnumerator.Current))
{
var expectedValue = hasExpected ? Format(expectedEnumerator.Current) : "";
var actualValue = hasActual ? Format(actualEnumerator.Current) : "";
throw new XFEAssertionException(message ?? $"Collections differ at index {index}. Expected {expectedValue}, actual {actualValue}.");
}
index++;
}
}
finally
{
(expectedEnumerator as IDisposable)?.Dispose();
(actualEnumerator as IDisposable)?.Dispose();
}
}
if (!EqualityComparer.Default.Equals(expected, actual))
throw new XFEAssertionException(message ?? $"Expected: {Format(expected)}{Environment.NewLine}Actual: {Format(actual)}");
}
///
/// 验证指定值不等于不期望出现的值。
///
/// 参与比较的值类型。
/// 不期望出现的值。
/// 实际值。
/// 断言失败时使用的可选消息。
/// 两个值相等。
public static void NotEqual(T notExpected, T actual, string? message = null)
{
if (EqualityComparer.Default.Equals(notExpected, actual))
throw new XFEAssertionException(message ?? $"Did not expect: {Format(actual)}");
}
///
/// 验证值为 。
///
/// 要验证的值。
/// 断言失败时使用的可选消息。
/// 不为 。
public static void Null(object? value, string? message = null)
{
if (value is not null)
throw new XFEAssertionException(message ?? $"Expected null, but found {Format(value)}.");
}
///
/// 验证值不为 ,并向可空流分析器声明成功后的非空状态。
///
/// 要验证的值。
/// 断言失败时使用的可选消息。
/// 为 。
public static void NotNull([NotNull] object? value, string? message = null)
{
if (value is null)
throw new XFEAssertionException(message ?? "Expected a non-null value.");
}
///
/// 验证两个值引用同一个对象实例。
///
/// 期望引用的对象。
/// 实际引用的对象。
/// 两个引用不指向同一个对象。
public static void Same(object? expected, object? actual)
{
if (!ReferenceEquals(expected, actual))
throw new XFEAssertionException("Expected both values to reference the same object.");
}
///
/// 验证序列中至少包含一个与期望值相等的元素。
///
/// 序列元素类型。
/// 期望包含的元素。
/// 要搜索的元素序列。
/// 序列中不存在期望元素。
public static void Contains(T expected, IEnumerable values)
{
if (!values.Contains(expected))
throw new XFEAssertionException($"Collection did not contain {Format(expected)}.");
}
///
/// 使用区分大小写的序号比较验证字符串包含指定子串。
///
/// 期望出现的子串。
/// 要搜索的完整字符串。
/// 实际字符串不包含期望子串。
public static void Contains(string expectedSubstring, string actual)
{
if (!actual.Contains(expectedSubstring, StringComparison.Ordinal))
throw new XFEAssertionException($"String did not contain {Format(expectedSubstring)}.");
}
///
/// 验证序列恰好包含一个元素并返回该元素。
///
/// 序列元素类型。
/// 要验证的序列。
/// 序列中的唯一元素。
/// 序列为空或包含多个元素。
public static T Single(IEnumerable values)
{
using var enumerator = values.GetEnumerator();
if (!enumerator.MoveNext())
throw new XFEAssertionException("Expected one item, but the collection was empty.");
var item = enumerator.Current;
if (enumerator.MoveNext())
throw new XFEAssertionException("Expected one item, but the collection contained multiple items.");
return item;
}
///
/// 对序列中的每个元素执行给定断言,并在失败消息中包含元素索引。
///
/// 序列元素类型。
/// 要逐项验证的序列。
/// 应用于每个元素的断言委托。
/// 任一元素的断言失败。
public static void All(IEnumerable values, Action assertion)
{
var index = 0;
foreach (var value in values)
{
try
{
assertion(value);
}
catch (Exception exception)
{
throw new XFEAssertionException($"Assertion failed for item {index}: {exception.Message}");
}
index++;
}
}
///
/// 验证可枚举对象不包含任何元素。
///
/// 要验证的可枚举对象。
/// 枚举器能够读取到至少一个元素。
public static void Empty(IEnumerable values)
{
var enumerator = values.GetEnumerator();
try
{
if (enumerator.MoveNext())
throw new XFEAssertionException("Expected an empty collection.");
}
finally
{
(enumerator as IDisposable)?.Dispose();
}
}
///
/// 验证值的运行时类型与指定类型完全一致并返回转换后的值。
///
/// 期望的精确运行时类型。
/// 要检查的值。
/// 转换为 的值。
/// 值为空、不可转换或其运行时类型不是 。
public static T IsType(object? value)
{
if (value is not T typed || value.GetType() != typeof(T))
throw new XFEAssertionException($"Expected exact type {typeof(T)}, but found {value?.GetType()}.");
return typed;
}
///
/// 验证值可赋给指定类型并返回转换后的值。
///
/// 期望可赋值到的类型。
/// 要检查的值。
/// 转换为 的值。
/// 值不能赋给 。
public static T AssignableFrom(object? value)
{
if (value is not T typed)
throw new XFEAssertionException($"Expected a value assignable to {typeof(T)}, but found {value?.GetType()}.");
return typed;
}
///
/// 验证同步委托抛出指定类型的异常并返回该异常。
///
/// 期望捕获的异常类型。
/// 应抛出异常的同步委托。
/// 委托抛出的 实例。
/// 委托没有抛出异常,或抛出了其他类型的异常。
public static TException Throws(Action action) where TException : Exception
{
try
{
action();
}
catch (TException exception)
{
return exception;
}
catch (Exception exception)
{
throw new XFEAssertionException($"Expected {typeof(TException).Name}, but caught {exception.GetType().Name}.");
}
throw new XFEAssertionException($"Expected {typeof(TException).Name}, but no exception was thrown.");
}
///
/// 完整等待异步委托,并验证其抛出指定类型的异常。
///
/// 期望捕获的异常类型。
/// 应异步抛出异常的 委托。
/// 表示异步断言的任务;结果为捕获到的异常实例。
/// 委托没有抛出异常,或抛出了其他类型的异常。
public static async Task ThrowsAsync(Func action) where TException : Exception
{
try
{
await action().ConfigureAwait(false);
}
catch (TException exception)
{
return exception;
}
catch (Exception exception)
{
throw new XFEAssertionException($"Expected {typeof(TException).Name}, but caught {exception.GetType().Name}.");
}
throw new XFEAssertionException($"Expected {typeof(TException).Name}, but no exception was thrown.");
}
///
/// 完整等待异步委托,并验证其抛出指定类型的异常。
///
/// 期望捕获的异常类型。
/// 应异步抛出异常的 委托。
/// 表示异步断言的值任务;结果为捕获到的异常实例。
/// 委托没有抛出异常,或抛出了其他类型的异常。
public static async ValueTask ThrowsAsync(Func action) where TException : Exception
{
try
{
await action().ConfigureAwait(false);
}
catch (TException exception)
{
return exception;
}
catch (Exception exception)
{
throw new XFEAssertionException($"Expected {typeof(TException).Name}, but caught {exception.GetType().Name}.");
}
throw new XFEAssertionException($"Expected {typeof(TException).Name}, but no exception was thrown.");
}
///
/// 验证可比较值位于包含上下边界的闭区间内。
///
/// 实现 的值类型。
/// 要检查的实际值。
/// 允许的最小值。
/// 允许的最大值。
/// 实际值小于下界或大于上界。
public static void InRange(T actual, T low, T high) where T : IComparable
{
if (actual.CompareTo(low) < 0 || actual.CompareTo(high) > 0)
throw new XFEAssertionException($"Expected {Format(actual)} to be in [{Format(low)}, {Format(high)}].");
}
private static string Format(object? value) => value?.ToString() ?? "";
}