From ecd3e199553425a317de8255b16aa87296a76156 Mon Sep 17 00:00:00 2001 From: Evangelink Date: Tue, 16 Jun 2026 16:06:30 +0200 Subject: [PATCH 1/2] Document Assert.Scope() soft assertions and related behaviors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...testing-mstest-writing-tests-assertions.md | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md b/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md index 16a7bb4cbf08c..055c6292b1da3 100644 --- a/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md +++ b/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md @@ -3,7 +3,7 @@ title: MSTest assertions description: Learn about MSTest assertions including Assert, StringAssert, and CollectionAssert classes for validating test results. author: Evangelink ms.author: amauryleve -ms.date: 07/15/2025 +ms.date: 06/16/2026 --- # MSTest assertions @@ -103,6 +103,72 @@ public async Task AssertExamples() - - +### Soft assertions with `Assert.Scope()` + +> [!IMPORTANT] +> `Assert.Scope()` is an experimental API. Using it produces the `MSTESTEXP` diagnostic, which you must suppress (for example, with `#pragma warning disable MSTESTEXP` or in your project's _.editorconfig_) to acknowledge that the shape and behavior of the API can change in future releases. + +By default, every assertion throws an as soon as it fails, which ends the test immediately. introduces *soft assertions*: while a scope is active, assertion failures are collected instead of thrown, so execution continues and you can see every failure in the scope at once. When the scope is disposed, the collected failures are reported together: + +```csharp +[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. +} +``` + +When the scope is disposed: + +- If exactly one failure was collected, the original `AssertFailedException` is thrown. +- If multiple failures were collected, a single `AssertFailedException` is thrown that wraps all of them in an `AggregateException`. + +#### Postconditions aren't enforced inside a scope + +Because a failing assertion no longer throws inside a scope, code that runs after it can't rely on the assertion having succeeded. This applies to *every* postcondition, including nullability and type narrowing: + +```csharp +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. +} +``` + +If a failed assertion would lead to a `NullReferenceException` (or any other exception) on a later line within the scope, that secondary exception is a symptom of the already-collected failure, not a separate bug. The original assertion failure is still reported when the scope is disposed. + +#### Value-returning assertions return `null`/`default` on failure inside a scope + +Some assertions return a value on success — for example, and return the caught exception, and returns the matched element. When one of these assertions *fails* inside a scope, the failure is collected and the method returns `null`/`default` instead of throwing: + +```csharp +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(() => { }); + _ = ex.Message; // NullReferenceException — don't use the return value in a scope +} +``` + +Don't rely on the value returned by a soft assertion inside a scope. If you need the returned value (such as the caught exception), call the assertion *outside* the scope, or restructure the test so nothing depends on the return value until after the scope is disposed. + +#### `Assert.Fail` and `Assert.Inconclusive` always throw + + and are never soft. They always throw immediately, even inside a scope, because they express an unconditional test outcome. Use one of them when a condition is critical and the rest of the test can't meaningfully continue without it. + +#### Nested scopes aren't supported + +You can't nest `Assert.Scope()` calls. Only one assertion scope can be active at a time. + ## The `StringAssert` class Use the class to compare and examine strings. From 10eae7764c7511cb0daed5dea425463c9f0b2da2 Mon Sep 17 00:00:00 2001 From: Evangelink Date: Tue, 16 Jun 2026 16:21:01 +0200 Subject: [PATCH 2/2] Address review: code-format .editorconfig, soften suppression wording, unspaced em dashes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../testing/unit-testing-mstest-writing-tests-assertions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md b/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md index 055c6292b1da3..ddcb376fe31eb 100644 --- a/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md +++ b/docs/core/testing/unit-testing-mstest-writing-tests-assertions.md @@ -106,7 +106,7 @@ public async Task AssertExamples() ### Soft assertions with `Assert.Scope()` > [!IMPORTANT] -> `Assert.Scope()` is an experimental API. Using it produces the `MSTESTEXP` diagnostic, which you must suppress (for example, with `#pragma warning disable MSTESTEXP` or in your project's _.editorconfig_) to acknowledge that the shape and behavior of the API can change in future releases. +> `Assert.Scope()` is an experimental API. Using it produces the `MSTESTEXP` diagnostic, which you suppress (for example, with `#pragma warning disable MSTESTEXP` or in your project's `.editorconfig` file) to acknowledge that the shape and behavior of the API can change in future releases. By default, every assertion throws an as soon as it fails, which ends the test immediately. introduces *soft assertions*: while a scope is active, assertion failures are collected instead of thrown, so execution continues and you can see every failure in the scope at once. When the scope is disposed, the collected failures are reported together: @@ -147,7 +147,7 @@ If a failed assertion would lead to a `NullReferenceException` (or any other exc #### Value-returning assertions return `null`/`default` on failure inside a scope -Some assertions return a value on success — for example, and return the caught exception, and returns the matched element. When one of these assertions *fails* inside a scope, the failure is collected and the method returns `null`/`default` instead of throwing: +Some assertions return a value on success—for example, and return the caught exception, and returns the matched element. When one of these assertions *fails* inside a scope, the failure is collected and the method returns `null`/`default` instead of throwing: ```csharp using (Assert.Scope()) @@ -155,7 +155,7 @@ 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(() => { }); - _ = ex.Message; // NullReferenceException — don't use the return value in a scope + _ = ex.Message; // NullReferenceException—don't use the return value in a scope } ```