Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/articles/nunit/writing-tests/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,6 @@ This table lists all the attributes supported by NUnit.
| [TestOf Attribute](xref:attribute-testof) | Indicates the name or Type of the class being tested. |
| [Theory Attribute](xref:attribute-theory) | Marks a test method as a Theory, a special kind of test in NUnit. |
| [Timeout Attribute](xref:attribute-timeout) | Provides a timeout value in milliseconds for test cases. |
| [UnhandledExceptionHandling Attribute](xref:attribute-unhandledexceptionhandling) | Controls whether unhandled exceptions on other threads fail the test. |
| [Values Attribute](xref:attribute-values) | Provides a set of inline values for a parameter of a test method. |
| [ValueSource Attribute](xref:attribute-valuesource) | Provides a source of values for a parameter of a test method. |
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ attribute allows you to change that behavior.
> This attribute only affects exceptions raised on threads other than the main test thread. Exceptions on the main test
> thread will always cause the test to fail, regardless of this attribute's setting.

## Constructors

```csharp
UnhandledExceptionHandlingAttribute(UnhandledExceptionHandling handling)
UnhandledExceptionHandlingAttribute(UnhandledExceptionHandling handling, params Type[] exceptions)
```

| Parameter | Type | Description |
| ------------ | ---------------------------- | ---------------------------------------------------------------------------------------------- |
| `handling` | `UnhandledExceptionHandling` | How NUnit treats a matching unhandled exception. |
| `exceptions` | `Type[]` | Exception types this entry applies to. When omitted, the entry applies to all exception types. |

The attribute may be applied more than once to the same element, so that different exception types get different
handling.

## Handling Modes

The attribute accepts an `UnhandledExceptionHandling` enum value:
Expand All @@ -34,6 +49,24 @@ The attribute accepts an `UnhandledExceptionHandling` enum value:
| ------------ | ----------------------- | -------- |
| ✅ | ✅ | ✅ |

## Platform Support

What this attribute can do depends on what the runtime allows.

* **.NET Framework:** an unhandled exception on another thread is reported to NUnit, which attributes it to the test
that was running and either records an error or, with `Ignore`, lets the test complete normally. This is the platform
the attribute was designed for, and the only one where `Ignore` keeps the process alive.
* **.NET Core and .NET 5 and later:** an unhandled exception on a thread **terminates the test process**, whatever this
attribute says. NUnit still receives the exception first and records an error when the mode is `Error`, but `Ignore`
cannot prevent the crash. .NET 10 added an API that lets a handler mark such an exception as handled, but NUnit does
not use it yet.
* **Unobserved `Task` exceptions:** these do not terminate the process on modern .NET. They are reported on the finalizer
thread, where NUnit usually cannot tell which test started the task, so they are logged and do not fail any test,
regardless of this attribute.

In practice the attribute matters for exceptions thrown on threads your test code creates itself, such as `new Thread`,
and for `ThreadAbortException` on .NET Framework.

## Basic Usage

### Error Mode (Default)
Expand Down Expand Up @@ -78,80 +111,60 @@ ignore `ThreadAbortException` specifically:
> `Thread.Abort()` is obsolete in .NET 5+ and throws `PlatformNotSupportedException` on those platforms.
> Consider migrating to `CancellationToken`-based cancellation patterns instead.

### Testing Fire-and-Forget Operations
### Background Threads Started by the Code Under Test (.NET Framework)

When testing code that intentionally spawns background work that might fail independently:
When the system under test starts its own threads and a failure on one of them is a known, accepted condition:

```csharp
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public void TestFireAndForgetLogging()
[TestFixture]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(ObjectDisposedException))]
public class LegacySystemTests
{
// The system under test fires off logging that we don't want to wait for
var sut = new ServiceWithBackgroundLogging();
var result = sut.DoWork();

Assert.That(result, Is.EqualTo(expected));
// Background logging exceptions won't fail this test
// The legacy component's cleanup thread sometimes throws ObjectDisposedException
// after the test has finished with it. On .NET Framework that no longer fails the test.
}
```

### Testing Cancellation Scenarios

When `OperationCanceledException` on background threads is expected and should be ignored:
On .NET Core and later the same exception would terminate the process, so the only real fix there is to stop the
exception from escaping the thread, for example by catching it in the thread procedure.

```csharp
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(OperationCanceledException))]
public async Task TestCancellationInBackgroundWork()
{
using var cts = new CancellationTokenSource();

// Fire-and-forget background work that observes the cancellation token
_ = Task.Run(() => LongRunningOperationAsync(cts.Token));
## Precedence When Applied at Several Levels

// Trigger cancellation while the test continues
cts.Cancel();

// Perform other assertions that do not await the background work
await Task.Delay(50);
Assert.That(SomeResult(), Is.EqualTo(expected));

// Any OperationCanceledException thrown on the background task
// will be ignored by NUnit due to the UnhandledExceptionHandling setting
}
```
The attribute can be applied at the assembly, fixture and method level. When an unhandled exception arrives, NUnit
looks for a matching entry starting at the **test method**, then the **fixture**, then the **assembly**, and uses the
first level that has one:

### Legacy Code Integration
1. At a given level, an entry that names the exception type wins over an entry that covers all exception types.
2. If a level has no entry for that exception, NUnit moves up to the next level.
3. If no level matches, the default `Error` applies.

When integrating with legacy code that has known background thread issues:
Settings are **not** merged across levels. A method-level `[UnhandledExceptionHandling(Ignore)]` covers every exception
type for that test, so a fixture-level entry for a specific type is never consulted for it. To ignore everything except
one type at the same level, give that type its own `Error` entry:

```csharp
[TestFixture]
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public class LegacySystemTests
[UnhandledExceptionHandling(UnhandledExceptionHandling.Error, typeof(InvalidOperationException))]
public void IgnoresEverythingExceptInvalidOperation()
{
// Legacy system has known issues with background thread cleanup
// that we accept for now
// ...
}
```

## Inheritance and Scope

The attribute can be applied at multiple levels:
## When to Use It

| Level | Scope |
| -------- | --------------------------------- |
| Assembly | Affects all tests in the assembly |
| Class | Affects all tests in the fixture |
| Method | Affects only the specific test |
> [!WARNING]
> Using `Ignore` mode can hide real bugs in your code. Use it sparingly and only when you understand the implications.

When multiple levels specify the attribute, they are combined - each level's configuration is additive.
The attribute is appropriate when:

## Best Practices
* You are on .NET Framework and test code that uses `Thread.Abort()`, so `ThreadAbortException` is expected.
* The code under test runs a background thread whose failure is already known, cannot be fixed in the test, and is
unrelated to what the test verifies. Name the exception type rather than ignoring everything.

> [!WARNING]
> Using `Ignore` mode can hide real bugs in your code. Use it sparingly and only when you understand the implications.
It is **not** a substitute for awaiting tasks or joining threads before the test ends. If the test can observe the
background work, do that instead, so a failure is reported where it happened.

1. **Prefer `Error` mode** - The default behavior ensures you're aware of all exceptions
2. **Be specific with exception types** - When using `Ignore`, specify only the exception types you expect
Expand All @@ -160,5 +173,6 @@ When multiple levels specify the attribute, they are combined - each level's con

## See Also

* [Assert.Throws](xref:classic-assert-throws)
* [Assert.ThrowsAsync](xref:classic-assert-throws-async)
* [CancelAfter Attribute](xref:attribute-cancelafter)
* [Timeout Attribute](xref:attribute-timeout)
* [Property Attribute](xref:attribute-property)
Original file line number Diff line number Diff line change
Expand Up @@ -27,37 +27,29 @@ public void TestWithErrorHandling()
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public void TestIgnoringBackgroundExceptions()
{
// Background exceptions are ignored - use with caution!
// This test will pass even if background threads throw
using var cts = new CancellationTokenSource();
var task = Task.Run(() =>
{
// Simulate work
Thread.Sleep(10);
}, cts.Token);
// An exception escaping this thread is not recorded against the test.
// On .NET Framework the test then passes; on .NET Core and later the
// runtime still terminates the process, so the thread procedure below
// must not actually throw.
var thread = new Thread(() => Thread.Sleep(10));
thread.Start();
thread.Join();

task.Wait();
Assert.Pass();
}
#endregion

#region UnhandledExceptionSpecificTypes
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(OperationCanceledException))]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(ObjectDisposedException))]
public void TestIgnoringSpecificExceptions()
{
// Only OperationCanceledException is ignored on background threads
// Other exception types will still cause the test to fail

_ = Task.Run(() =>
{
// This will throw on a background thread
Thread.Sleep(10);
throw new OperationCanceledException();
});
// Only ObjectDisposedException escaping a background thread is ignored.
// Any other exception type is still recorded as a test error.
var thread = new Thread(() => Thread.Sleep(10));
thread.Start();
thread.Join();

// The unhandled OperationCanceledException on the background thread
// will be ignored by the attribute, so this test still passes
Assert.Pass();
}
#endregion
Expand Down
Loading