XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.XUnit

【DLL】提供方便快捷的测试,无需编写Main方法,可直接添加特性在类或方法上进行测试

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections;
using System.Diagnostics.CodeAnalysis;

namespace XFEExtension.NetCore.XUnit.Assertions;

/// <summary>
/// 提供不依赖全局状态的强类型测试断言;断言失败时抛出 <see cref="XFEAssertionException"/>。
/// </summary>
public static class Assert
{
    /// <summary>
    /// 验证条件为 <see langword="true"/>。
    /// </summary>
    /// <param name="condition">要验证的布尔条件。</param>
    /// <param name="message">断言失败时使用的可选消息。</param>
    /// <exception cref="XFEAssertionException"><paramref name="condition"/> 为 <see langword="false"/>。</exception>
    public static void True([DoesNotReturnIf(false)] bool condition, string? message = null)
    {
        if (!condition)
            throw new XFEAssertionException(message ?? "Expected true, but found false.");
    }

    /// <summary>
    /// 验证条件为 <see langword="false"/>。
    /// </summary>
    /// <param name="condition">要验证的布尔条件。</param>
    /// <param name="message">断言失败时使用的可选消息。</param>
    /// <exception cref="XFEAssertionException"><paramref name="condition"/> 为 <see langword="true"/>。</exception>
    public static void False([DoesNotReturnIf(true)] bool condition, string? message = null)
    {
        if (condition)
            throw new XFEAssertionException(message ?? "Expected false, but found true.");
    }

    /// <summary>
    /// 验证期望值与实际值相等;对于非字符串可枚举值,会按顺序逐项比较。
    /// </summary>
    /// <typeparam name="T">参与比较的值类型。</typeparam>
    /// <param name="expected">期望值。</param>
    /// <param name="actual">实际值。</param>
    /// <param name="message">断言失败时使用的可选消息。</param>
    /// <exception cref="XFEAssertionException">两个值不相等,或两个序列的长度或元素不同。</exception>
    public static void Equal<T>(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) : "<end of collection>";
                        var actualValue = hasActual ? Format(actualEnumerator.Current) : "<end of collection>";
                        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<T>.Default.Equals(expected, actual))
            throw new XFEAssertionException(message ?? $"Expected: {Format(expected)}{Environment.NewLine}Actual:   {Format(actual)}");
    }

    /// <summary>
    /// 验证指定值不等于不期望出现的值。
    /// </summary>
    /// <typeparam name="T">参与比较的值类型。</typeparam>
    /// <param name="notExpected">不期望出现的值。</param>
    /// <param name="actual">实际值。</param>
    /// <param name="message">断言失败时使用的可选消息。</param>
    /// <exception cref="XFEAssertionException">两个值相等。</exception>
    public static void NotEqual<T>(T notExpected, T actual, string? message = null)
    {
        if (EqualityComparer<T>.Default.Equals(notExpected, actual))
            throw new XFEAssertionException(message ?? $"Did not expect: {Format(actual)}");
    }

    /// <summary>
    /// 验证值为 <see langword="null"/>。
    /// </summary>
    /// <param name="value">要验证的值。</param>
    /// <param name="message">断言失败时使用的可选消息。</param>
    /// <exception cref="XFEAssertionException"><paramref name="value"/> 不为 <see langword="null"/>。</exception>
    public static void Null(object? value, string? message = null)
    {
        if (value is not null)
            throw new XFEAssertionException(message ?? $"Expected null, but found {Format(value)}.");
    }

    /// <summary>
    /// 验证值不为 <see langword="null"/>,并向可空流分析器声明成功后的非空状态。
    /// </summary>
    /// <param name="value">要验证的值。</param>
    /// <param name="message">断言失败时使用的可选消息。</param>
    /// <exception cref="XFEAssertionException"><paramref name="value"/> 为 <see langword="null"/>。</exception>
    public static void NotNull([NotNull] object? value, string? message = null)
    {
        if (value is null)
            throw new XFEAssertionException(message ?? "Expected a non-null value.");
    }

    /// <summary>
    /// 验证两个值引用同一个对象实例。
    /// </summary>
    /// <param name="expected">期望引用的对象。</param>
    /// <param name="actual">实际引用的对象。</param>
    /// <exception cref="XFEAssertionException">两个引用不指向同一个对象。</exception>
    public static void Same(object? expected, object? actual)
    {
        if (!ReferenceEquals(expected, actual))
            throw new XFEAssertionException("Expected both values to reference the same object.");
    }

    /// <summary>
    /// 验证序列中至少包含一个与期望值相等的元素。
    /// </summary>
    /// <typeparam name="T">序列元素类型。</typeparam>
    /// <param name="expected">期望包含的元素。</param>
    /// <param name="values">要搜索的元素序列。</param>
    /// <exception cref="XFEAssertionException">序列中不存在期望元素。</exception>
    public static void Contains<T>(T expected, IEnumerable<T> values)
    {
        if (!values.Contains(expected))
            throw new XFEAssertionException($"Collection did not contain {Format(expected)}.");
    }

    /// <summary>
    /// 使用区分大小写的序号比较验证字符串包含指定子串。
    /// </summary>
    /// <param name="expectedSubstring">期望出现的子串。</param>
    /// <param name="actual">要搜索的完整字符串。</param>
    /// <exception cref="XFEAssertionException">实际字符串不包含期望子串。</exception>
    public static void Contains(string expectedSubstring, string actual)
    {
        if (!actual.Contains(expectedSubstring, StringComparison.Ordinal))
            throw new XFEAssertionException($"String did not contain {Format(expectedSubstring)}.");
    }

    /// <summary>
    /// 验证序列恰好包含一个元素并返回该元素。
    /// </summary>
    /// <typeparam name="T">序列元素类型。</typeparam>
    /// <param name="values">要验证的序列。</param>
    /// <returns>序列中的唯一元素。</returns>
    /// <exception cref="XFEAssertionException">序列为空或包含多个元素。</exception>
    public static T Single<T>(IEnumerable<T> 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;
    }

    /// <summary>
    /// 对序列中的每个元素执行给定断言,并在失败消息中包含元素索引。
    /// </summary>
    /// <typeparam name="T">序列元素类型。</typeparam>
    /// <param name="values">要逐项验证的序列。</param>
    /// <param name="assertion">应用于每个元素的断言委托。</param>
    /// <exception cref="XFEAssertionException">任一元素的断言失败。</exception>
    public static void All<T>(IEnumerable<T> values, Action<T> 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++;
        }
    }

    /// <summary>
    /// 验证可枚举对象不包含任何元素。
    /// </summary>
    /// <param name="values">要验证的可枚举对象。</param>
    /// <exception cref="XFEAssertionException">枚举器能够读取到至少一个元素。</exception>
    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();
        }
    }

    /// <summary>
    /// 验证值的运行时类型与指定类型完全一致并返回转换后的值。
    /// </summary>
    /// <typeparam name="T">期望的精确运行时类型。</typeparam>
    /// <param name="value">要检查的值。</param>
    /// <returns>转换为 <typeparamref name="T"/> 的值。</returns>
    /// <exception cref="XFEAssertionException">值为空、不可转换或其运行时类型不是 <typeparamref name="T"/>。</exception>
    public static T IsType<T>(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;
    }

    /// <summary>
    /// 验证值可赋给指定类型并返回转换后的值。
    /// </summary>
    /// <typeparam name="T">期望可赋值到的类型。</typeparam>
    /// <param name="value">要检查的值。</param>
    /// <returns>转换为 <typeparamref name="T"/> 的值。</returns>
    /// <exception cref="XFEAssertionException">值不能赋给 <typeparamref name="T"/>。</exception>
    public static T AssignableFrom<T>(object? value)
    {
        if (value is not T typed)
            throw new XFEAssertionException($"Expected a value assignable to {typeof(T)}, but found {value?.GetType()}.");
        return typed;
    }

    /// <summary>
    /// 验证同步委托抛出指定类型的异常并返回该异常。
    /// </summary>
    /// <typeparam name="TException">期望捕获的异常类型。</typeparam>
    /// <param name="action">应抛出异常的同步委托。</param>
    /// <returns>委托抛出的 <typeparamref name="TException"/> 实例。</returns>
    /// <exception cref="XFEAssertionException">委托没有抛出异常,或抛出了其他类型的异常。</exception>
    public static TException Throws<TException>(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.");
    }

    /// <summary>
    /// 完整等待异步委托,并验证其抛出指定类型的异常。
    /// </summary>
    /// <typeparam name="TException">期望捕获的异常类型。</typeparam>
    /// <param name="action">应异步抛出异常的 <see cref="Task"/> 委托。</param>
    /// <returns>表示异步断言的任务;结果为捕获到的异常实例。</returns>
    /// <exception cref="XFEAssertionException">委托没有抛出异常,或抛出了其他类型的异常。</exception>
    public static async Task<TException> ThrowsAsync<TException>(Func<Task> 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.");
    }

    /// <summary>
    /// 完整等待异步委托,并验证其抛出指定类型的异常。
    /// </summary>
    /// <typeparam name="TException">期望捕获的异常类型。</typeparam>
    /// <param name="action">应异步抛出异常的 <see cref="ValueTask"/> 委托。</param>
    /// <returns>表示异步断言的值任务;结果为捕获到的异常实例。</returns>
    /// <exception cref="XFEAssertionException">委托没有抛出异常,或抛出了其他类型的异常。</exception>
    public static async ValueTask<TException> ThrowsAsync<TException>(Func<ValueTask> 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.");
    }

    /// <summary>
    /// 验证可比较值位于包含上下边界的闭区间内。
    /// </summary>
    /// <typeparam name="T">实现 <see cref="IComparable{T}"/> 的值类型。</typeparam>
    /// <param name="actual">要检查的实际值。</param>
    /// <param name="low">允许的最小值。</param>
    /// <param name="high">允许的最大值。</param>
    /// <exception cref="XFEAssertionException">实际值小于下界或大于上界。</exception>
    public static void InRange<T>(T actual, T low, T high) where T : IComparable<T>
    {
        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() ?? "<null>";
}