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() ?? ""; }