Centralized Exception Reporting in NUnit

Software systems often require the ability to centrally configure common features, or cross-cutting concerns, like logging and other observability hooks. Many application frameworks allow for this by providing hooks into their execution pipelines to allow for registration and execution of customized middleware code. This can also be desirable in test systems. In this post I’ll show how this can be done in NUnit using the Execution Hooks feature recently introduced in version 4.5.

Overview

NUnit defines a range of available hooks within the ExecutionHookAttribute class. The hooks can be implemented by defining a custom attribute which extends this class and overriding the desired method(s). These pipeline methods all follow the “BeforeX” and “AfterX” paradigm to allow for custom code to run before or after any of the NUnit test lifecycle states. Each defined before hook has a corresponding after hook to support scoped management of resources. Some examples of these hooks are BeforeEverySetUpHook, BeforeTestHook, and AfterTestHook.

One of the many use cases for using execution hooks may be that you have a large integration test suite and wish to log any uncaught exceptions that prevented tests from completing. This particular problem can be solved by overriding the AfterTestHook method and checking the Exception property of the HookData instance which gets passed in. The custom attribute can then be configured to target an “assembly” so that it only needs to be defined once for an entire suite. Other standard attribute targets in .NET which may be useful for other scenarios are “class” or “method”.

Example

Combining these concepts may look something like below.

// Allow for this behaviour to be defined once-per-assembly
[AttributeUsage(AttributeTargets.Assembly)]
public sealed class CentralizedExceptionLoggingAttribute : ExecutionHookAttribute
{
static ExceptionLoggingHookAttribute()
{
// Setup the output file
var dir = TestContext.CurrentContext.WorkDirectory;
File.Delete(Path.Combine(dir, "FailedTests.txt"));
}
public override void AfterTestHook(HookData hookData)
{
if (hookData.Exception is not null)
{
// Write to output file
var dir = hookData.Context.WorkDirectory;
File.AppendAllLines(Path.Combine(dir, "FailedTests.txt"), [hookData.Context.Test.FullName]);
// Write to standard output
TestContext.Out.WriteLine($"Exception thrown in test ({hookData.Context.Test.Name}). Details:");
TestContext.Out.WriteLine(hookData.Exception);
}
}
}

This will output error info to the standard output stream and separately write a list of failed tests to a file for later reference or rerunning. It can be easily modified to support more complex requirements by using a standard logging library like Serilog, NLog, or log4net.

Sample usage:

[assembly: CentralizedExceptionLogging]

This is just one of many possible uses. A full list of the 10 hooks can be found in the documentation, along with several other example use cases. More information on using hooks for exception handling can also be found here.

Conclusion

In conclusion, the execution hooks feature introduced in NUnit 4.5 offers a wide range of customization and uses to gain visibility into what is happening in your test suites. It can also be used as a basis to customize test behaviours, execution contexts, and many other features.

Comments

Leave a Reply

Discover more from Software by Steven

Subscribe now to keep reading and get access to the full archive.

Continue reading