MSTest 斷言

使用 Assert 命名空間的 Microsoft.VisualStudio.TestTools.UnitTesting 類別來驗證特定功能。 測試方法在你的應用程式中執行程式碼,但只有在你加入 Assert 陳述式時才會報告正確性。

概觀

MSTest 提供三種斷言類別:

Class 目標
Assert 值、型別與例外的通用斷言。
StringAssert 字串專屬的斷言,用於模式、子字串與比較。
CollectionAssert 用於比較與驗證集合的集合斷言。

這很重要

新程式碼一定要用類別 AssertStringAssert 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.ContainsAssert.DoesNotContainAssert.HasCountAssert.IsEmptyAssert.IsNotEmptyAssert.ContainsSingle

從 MSTest 3.10 開始,比較斷言包括 Assert.IsInRangeAssert.IsGreaterThanAssert.IsGreaterThanOrEqualToAssert.IsLessThanAssert.IsLessThanOrEqualToAssert.IsPositiveAssert.IsNegative

從 MSTest 3.10 開始,字串匹配斷言包括 Assert.StartsWithAssert.EndsWithAssert.MatchesRegexAssert.DoesNotStartWithAssert.DoesNotEndWithAssert.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.IsTrueAssert.IsFalseAssert.IsNullAssert.IsNotNull 的結構化斷言失敗訊息,其中包含已求值的運算式。
  • 非同步 Assert.ThrowsAsync/Assert.ThrowsExactlyAsync 方法的插值字串訊息過載,以及拒絕 ValueTask<TResult>原本不會等待的 -回傳代理。
  • 完整的例外細節,包括堆疊追蹤與內部例外,皆包含 Assert.Throws* 失敗訊息中的內容。
  • 斷言失敗堆疊,能隱藏 MSTest 實作幀,並以全精度渲染內建數值。

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(或任何其他例外),那麼該次要例外只是先前已收集到的失敗所呈現的症狀,而不是另一個獨立的錯誤。 當作用範圍被釋放時,原始的斷言失敗仍會被回報。

具回傳值的斷言在作用域內失敗時會回傳 null/default

有些斷言會在成功時回傳值,例如 ThrowsThrowsExactly 會回傳攔截到的例外,而 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 永遠要投擲

FailInconclusive 絕不會是軟的。 即使在示波器內,他們總是立即拋出,因為他們表達的是無條件的測試結果。 當某個狀況很危急,且其他檢查無法有效進行時,才用其中一種。

巢狀示波器不被支援

你不能巢狀呼叫 Assert.Scope()。 一次只能啟用一個斷言範圍。

StringAssert 類別

使用 StringAssert 類別來比較並檢查字串。

警告

這個 StringAssert 類別很可能會在未來版本中被淘汰。 它只是為了向下相容而維護,不建議用於新程式碼。 所有 StringAssert 方法在 Assert 類別中都有對應的方法,讓它們更容易被找到。 若要移轉現有用法,請參閱分析器 MSTEST0046

可用的 API 包括:

CollectionAssert 類別

使用 CollectionAssert 類別來比較物件集合,或確認集合的狀態。

警告

這個 CollectionAssert 類別很可能會在未來版本中被淘汰。 它主要是為了向下相容而維護,不建議用於新程式碼。 當 上 Assert 存在等價方法(例如 Assert.ContainsAssert.DoesNotContain、 或 Assert.HasCount),則使用 Assert 以提升可發現性。

可用的 API 包括:

建立自訂斷言 Assert.That

內建的斷言方法無法涵蓋所有情境。 若要使用您自己的檢查來擴充斷言基礎結構,MSTest 會公開 Assert.That 單例屬性作為擴充點。 你可以在實例型別上以 C# 擴充方法 Assert 的方式新增自訂斷言,呼叫者則以熟悉 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);
}

StringAssertCollectionAssert 上的擴充掛鉤

StringAssert.ThatCollectionAssert.That 屬性為了向後相容性,公開了相同的單例模式。 對於新的自訂斷言,一律以 Assert.That 為目標。 否則,你的助手會繼承和舊有類別一樣的可偵測性問題,如果 StringAssertCollectionAssert 被棄用,他們就需要遷移。

Assert.That 性質與 Assert.That(...) 方法

備註

不要將Assert.That單例屬性(作為擴充點)與 MSTest 3.8 中新增的 Assert.That(() => condition)方法混淆。 後者接受布林運算式,並透過分析表達式樹產生詳細的失敗訊息(例如, Assert.That(() => order.Total > 0))。 這兩個 API 名稱相同,但功能不同。

最佳做法

  • 使用具體的斷言:偏好 AreEqual over IsTrue(a == b) 以獲得更好的失敗訊息。

  • 包含描述性訊息:透過明確的斷言訊息幫助快速識別失敗。

  • 一次測試一項:每個測試方法都應該驗證單一行為。

  • 在 MSTest v3.8+ 中,使用Throws/ThrowsExactly來處理例外情況時,偏好Assert.Throws及其非同步版本(Assert.ThrowsExactlyThrowsAsync),而非使用ThrowsExactlyAsync屬性。

  • 偏好 Assert 高於 StringAssert/CollectionAssert:為了更好的發現性和一致性,請使用該 Assert 類別。 StringAssert CollectionAssert這些類別很可能會在未來版本中被棄用。

  • 擴充 Assert.That 自訂斷言:為了保持一致的可發現性,請將自訂斷言作為擴充方法加入 Assert ,並透過 Assert.That呼叫。 不要在新程式碼中以 StringAssert.ThatCollectionAssert.That 為目標。

以下分析器有助於確保斷言的正確使用:

  • MSTEST0006 - 避免 ExpectedException 屬性,改用 Assert.Throws 方法。
  • MSTEST0017 - 斷言論證應依正確順序傳遞。
  • MSTEST0023 - 不要否定布林值斷言。
  • MSTEST0025 - 偏好使用 Assert.Fail,而不是永遠錯誤的條件。
  • MSTEST0026 - 斷言論證應避免條件存取。
  • MSTEST0032 - 檢視永遠為真的斷言條件。
  • MSTEST0037 - 使用正確的斷言方法。
  • MSTEST0038 - 避免 Assert.AreSame 使用價值類型。
  • MSTEST0039 - 使用更新 Assert.Throws 的方法。
  • MSTEST0040 - 避免在非同步 void 上下文中使用斷言。
  • MSTEST0046 - 用 Assert 代替 StringAssert
  • MSTEST0051 - Assert.Throws 應該包含一個陳述。
  • MSTEST0053 - 避免 Assert 格式參數。
  • MSTEST0058 - 避免在捕捉區塊中使用斷言。

另請參閱