MSTest 断言

使用 Assert 命名空间的 Microsoft.VisualStudio.TestTools.UnitTesting 类来验证特定功能。 测试方法运行应用程序中的代码,但只有在包含 Assert 语句时才报告正确性。

概述

MSTest 提供三个断言类:

Class 目的
Assert 通用的值、类型和异常断言。
StringAssert 针对模式、子字符串和比较的字符串特定断言。
CollectionAssert 用于比较和验证集合的集合断言。

重要

对于新代码,请始终使用该 Assert 类。 StringAssert 和 CollectionAssert 类可能会在未来的某个版本中被弃用。 它们主要用于向后兼容,但不建议这样做,因为跨三种类型拆分断言会损害可发现性。

所有断言方法都接受一个可选消息参数,该参数在断言失败时显示,有助于确定原因:

Assert.AreEqual(expected, actual, "Values should match after processing");

Assert 类

使用 Assert 类验证所测试的代码是否按预期方式运行。

注释

从 MSTest 4.0 开始,所有 Assert API 捕获参数表达式并将其包含在失败消息中。 此支持在没有手动 message 参数的情况下提供更丰富的诊断。

常见断言方法

[TestMethod]
public async Task AssertExamples()
{
    // Equality
    Assert.AreEqual(5, calculator.Add(2, 3));
    Assert.AreNotEqual(0, result);

    // Reference equality
    Assert.AreSame(expected, actual);
    Assert.AreNotSame(obj1, obj2);

    // Boolean conditions
    Assert.IsTrue(result > 0);
    Assert.IsFalse(string.IsNullOrEmpty(name));

    // Null checks
    Assert.IsNull(optionalValue);
    Assert.IsNotNull(requiredValue);

    // Type checks
    Assert.IsInstanceOfType<IDisposable>(obj);
    Assert.IsNotInstanceOfType<string>(obj);

    // Exception testing (MSTest v3.8+)
    Assert.ThrowsExactly<ArgumentNullException>(() => service.Process(null!));
    await Assert.ThrowsExactlyAsync<InvalidOperationException>(
        async () => await service.ProcessAsync());
}

Assert.That 方法

从 MSTest 4.0 开始, Assert.That 计算任何布尔表达式并生成明确的失败消息。 为了提供更丰富的诊断信息,Assert.That 使用 [CallerArgumentExpression] 自动捕获表达式文本。

Assert.That(order.Total > 0);

可用 API

注释

从 MSTest 3.8 开始,集合断言包括Assert.Contains、Assert.DoesNotContain、Assert.HasCount、Assert.IsEmpty和Assert.IsNotEmptyAssert.ContainsSingle。

从 MSTest 3.10 开始,比较断言包括Assert.IsInRange、Assert.IsGreaterThan、Assert.IsGreaterThanOrEqualTo、Assert.IsLessThan、Assert.IsLessThanOrEqualTo和Assert.IsPositiveAssert.IsNegative。

从 MSTest 3.10 开始,字符串匹配断言包括 Assert.StartsWith、Assert.EndsWith、Assert.MatchesRegex、Assert.DoesNotStartWith、Assert.DoesNotEndWith 和 Assert.DoesNotMatchRegex。

从 MSTest 4.1 开始, Assert.IsExactInstanceOfTypeAssert.IsNotExactInstanceOfType 需要完全类型匹配。 与 Assert.IsInstanceOfType 不同,这些方法不匹配派生类型。

MSTest 4.3 中的新集合和等效断言

注释

MSTest 4.3.0 中引入了以下断言方法。

  • Assert.AreSequenceEqual / Assert.AreNotSequenceEqual - 元素顺序比较。 传入 SequenceOrder.InAnyOrder 以忽略元素顺序。
  • Assert.AreEquivalent / Assert.AreNotEquivalent — 两个对象或集合的深层结构比较。
  • Assert.ContainsAll / Assert.DoesNotContainAll — 断言集合包含每个预期元素(或不包含)。
  • Assert.AreAllNotNull — 断言集合的每个元素都是非null元素。
  • Assert.AreAllDistinct — 断言集合的所有元素都是不同的。
  • Assert.AreAllOfType — 断言集合的每个元素都为预期类型。

比较集合时,应优先使用这些方法,而不是使用 Assert.AreEqual,因为它比较的是引用而不是元素。

MSTest 4.3 还添加了:

  • 用于自定义断言失败消息中值的呈现方式的实验性 Assert.AddValueFormatter API。
  • Span<T> 的 Memory<T> 和 Assert.HasCount 重载
  • 针对 Assert.IsTrue、Assert.IsFalse、Assert.IsNull 和 Assert.IsNotNull 的结构化断言失败消息,其中包含求值后的表达式。
  • 为异步 Assert.ThrowsAsync/Assert.ThrowsExactlyAsync 方法重载了插值字符串消息,并拒绝了那些原本不会被等待的、返回 ValueTask<TResult> 的委托。
  • Assert.Throws* 失败消息中的完整异常详细信息,包括堆栈跟踪和内部异常。
  • 隐藏 MSTest 实现堆栈帧并以完整精度显示内置数值的断言失败堆栈

重要

以下断言重载计划用于 MSTest 4.4,仅在预览版本中可用,直到 MSTest 4.4.0 发布。

MSTest 4.4 向其余集合 API 添加了跨度和内存重载Assert.IsEmptyAssert.IsNotEmpty。 这些重载接受 Span<T>、ReadOnlySpan<T>、Memory<T> 和 ReadOnlyMemory<T>:

  • 全项检查:AreAllDistinct、AreAllNotNull和AreAllOfType。
  • 比较: AreEquivalent、 AreNotEquivalent、 AreSequenceEqual和 AreNotSequenceEqual。
  • 包含:Contains、ContainsAll、ContainsSingle、DoesNotContain和DoesNotContainAll。

在 MSTest 4.4 和 MTP 2.4 预览版中,支持此功能的 IDE 和报告器可将断言的预期值和实际值作为单独的结构化属性接收。 使用者无需分析失败消息中的这些值。

Assert.AddValueFormatter 返回一个 IDisposable 注册项。 释放注册以删除格式化程序。 格式化程序仅适用于当前异步上下文,因此并行测试可以使用不同的格式化程序,而无需更改彼此的输出。 由于 API 在 MSTest 4.3 中是实验性的,因此在使用它之前,请确认或取消 MSTESTEXP 诊断。

使用 Assert.Scope() 的软断言

重要

Assert.Scope() 是一个实验 API。 使用它会产生 MSTESTEXP 诊断,你需要抑制它(例如,使用 #pragma warning disable MSTESTEXP 或在项目的 .editorconfig 文件中),以确认 API 的形状和行为在未来版本中可能会更改。

默认情况下,每个断言在失败后立即引发 AssertFailedException ,这会立即结束测试。 Assert.Scope() 引入了 软断言:在作用域处于活动状态期间,断言失败会被收集起来,而不是被抛出,因此执行会继续进行,你可以一次性看到该作用域中的所有失败。 当作用域被释放时,收集到的失败会一起报告:

[TestMethod]
public void ValidatePerson()
{
    using (Assert.Scope())
    {
        Assert.AreEqual("Jane", person.FirstName); // failure collected, execution continues
        Assert.AreEqual("Doe", person.LastName);   // failure collected, execution continues
        Assert.IsTrue(person.IsActive);            // failure collected, execution continues
    }
    // On Dispose, all collected failures are reported together.
}

当作用域被释放时:

  • 如果恰好收集到一个失败,则抛出原始的 AssertFailedException。
  • 如果收集到多个失败,则抛出一个 AssertFailedException,其中包含所有失败,包裹在 AggregateException 中。

在作用域内不会强制执行后置条件

由于失败的断言在作用域内不再抛出,因此在其后运行的代码不能依赖于断言已成功。 这适用于所有后置条件,包括可空性和类型收窄:

using (Assert.Scope())
{
    Assert.IsNotNull(item);
    // 'item' might still be null here: the failure was collected, not thrown.
    Assert.AreEqual("expected", item.Value);
    // 'item.Value' might not equal "expected" either.
}

如果失败的断言会导致作用域内后续行出现 NullReferenceException(或任何其他异常),则该次要异常是已收集失败的征兆,而不是一个独立的 bug。 当作用域被释放时,原始的断言失败仍会被报告。

在作用域内失败时,返回值断言会返回 null/default

某些断言返回成功值,例如, Throws 返回 ThrowsExactly 捕获的异常,并 ContainsSingle 返回匹配的元素。 当这些断言之一在作用域内失败时,失败会被收集,方法会返回 null/default,而不是抛出:

using (Assert.Scope())
{
    // No exception is thrown by the lambda, so the assertion fails. The failure is
    // collected and 'ex' is null. Accessing 'ex' below throws NullReferenceException.
    InvalidOperationException ex = Assert.Throws<InvalidOperationException>(() => { });
    _ = ex.Message; // NullReferenceException—don't use the return value in a scope
}

不要依赖作用域内软断言返回的值。 如果你需要返回的值(如捕获的异常),请在作用域外调用断言,或者重构测试,直到作用域释放后才依赖返回值。

Assert.Fail 和 Assert.Inconclusive 总是抛出

Fail 和 Inconclusive 都不是软性的。 即使在作用域内,它们也总是立即抛出,因为它们表达了无条件测试的结果。 当某个条件至关重要,且如果没有它,测试的其余部分就无法有意义地继续进行时,请使用其中之一。

不支持嵌套作用域

你不能嵌套 Assert.Scope() 调用。 一次只能有一个断言作用域处于激活状态。

StringAssert 类

使用 StringAssert 类来比较和检查字符串。

警告

StringAssert 类可能会在未来的版本中被弃用。 它仅用于向后兼容性,不建议用于新代码。 所有 StringAssert 方法对 Assert 类具有等效项,可提供更好的可发现性。 若要迁移现有使用情况,请参阅分析器 MSTEST0046。

可用的 API 包括:

CollectionAssert 类

使用 CollectionAssert 类比较对象的集合,并验证集合的状态。

警告

CollectionAssert 类可能会在未来的版本中被弃用。 它主要用于向后兼容性,不建议用于新代码。 如果 Assert 上存在等效方法(例如 Assert.Contains、Assert.DoesNotContain 或 Assert.HasCount),请使用 Assert,以提高可发现性。

可用的 API 包括:

使用 Assert.That 创建自定义断言

内置断言方法并不涵盖每个方案。 若要通过自定义检查扩展断言基础设施,MSTest 将 Assert.That 单例属性作为可扩展性挂钩提供。 你可以在 Assert 实例类型上将自定义断言添加为 C# 扩展方法,调用方则使用熟悉的 Assert.That.MyAssertion(...) 语法来调用这些断言。

为了提高可发现性,在专用静态类中组织项目范围的断言。 通过 Assert.That 访问的自定义断言与 IntelliSense 中的内置方法一起显示,因此使用者不必记住单独的帮助程序类型。

创作自定义断言

添加一个以 Assert 类型为目标的扩展方法,并在条件失败时抛出 AssertFailedException:

using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

public static class CustomAssertExtensions
{
    public static void IsPrime(this Assert assert, int value)
    {
        if (value < 2 || Enumerable.Range(2, (int)Math.Sqrt(value) - 1).Any(i => value % i == 0))
        {
            throw new AssertFailedException($"Assert.That.IsPrime failed. Value <{value}> is not a prime number.");
        }
    }
}

使用自定义断言

导入包含扩展方法的命名空间后,请通过以下方法 Assert.That调用自定义断言:

[TestMethod]
public void Compute_ReturnsPrime()
{
    int result = _calculator.NextPrime(10);
    Assert.That.IsPrime(result);
}

StringAssert 和 CollectionAssert 上的扩展钩子

StringAssert.That 和 CollectionAssert.That 属性为了向后兼容,提供了相同的单例模式。 对于新的自定义断言,始终以 Assert.That 为目标。 否则,这些辅助程序将继承与旧类相同的可发现性问题,而且如果 StringAssert 和 CollectionAssert 被弃用,它们也需要迁移。

Assert.That 属性与 Assert.That(...) 方法

注释

不要将 Assert.That单例属性(用作扩展点)与 MSTest 3.8 中新增的 Assert.That(() => condition)方法混淆。 后者接受布尔表达式,并通过分析表达式树(例如, Assert.That(() => order.Total > 0))生成详细的失败消息。 这两个 API 共享一个名称,但用途不同。

最佳做法

  • 使用特定断言:优先使用 AreEqual 而不是 IsTrue(a == b),以获得更好的故障消息。

  • 包括描述性消息:帮助使用明确的断言消息快速识别故障。

  • 一次测试一件事:每个测试方法都应验证单个行为。

  • 使用 Throws/ThrowsExactly来处理异常:在 MSTest v3.8+ 中,优先使用Assert.ThrowsAssert.ThrowsExactly及其异步对应的 ThrowsAsyncThrowsExactlyAsync,而不是 ExpectedException 属性。

  • 优先使用 Assert,而不是 StringAssert/CollectionAssert:为了提高可发现性和一致性,请使用 Assert 类。 StringAssert 和 CollectionAssert 类可能会在未来的某个版本中被弃用。

  • 为自定义断言扩展 Assert.That:为了确保一致的可发现性,请将自定义断言添加为 Assert 上的扩展方法,并通过 Assert.That 调用它们。 不要在新代码中以 StringAssert.That 或 CollectionAssert.That 为目标。

以下分析器有助于确保正确使用断言:

  • MSTEST0006 - 避免 ExpectedException 属性,改用 Assert.Throws 方法。
  • MSTEST0017 - 断言参数应按正确的顺序传递。
  • MSTEST0023 - 不要否定布尔断言。
  • MSTEST0025 - 优先使用 Assert.Fail 而不是始终为假的条件。
  • MSTEST0026 - 断言参数应避免条件访问。
  • MSTEST0032 - 审查始终为真的断言条件。
  • MSTEST0037 - 使用正确的断言方法。
  • MSTEST0038 - 避免 Assert.AreSame 与值类型一起使用。
  • MSTEST0039 - 使用较 Assert.Throws 新的方法。
  • MSTEST0040 - 避免在异步 void 上下文中使用断言。
  • MSTEST0046 - 使用 Assert 而不是 StringAssert。
  • - Assert.Throws MSTEST0051应包含单个语句。
  • MSTEST0053 - 避免 Assert 格式参数。
  • MSTEST0058 - 避免在 catch 块中使用断言。

另请参阅