diff --git a/docs/articles/nunit/extending-nunit/Execution-Hooks.md b/docs/articles/nunit/extending-nunit/Execution-Hooks.md index 44f3cc8f9..092b0fc05 100644 --- a/docs/articles/nunit/extending-nunit/Execution-Hooks.md +++ b/docs/articles/nunit/extending-nunit/Execution-Hooks.md @@ -47,6 +47,16 @@ Each hook receives a `HookData` instance: Use these fields for logging, conditional logic, or adaptive cleanup. +## One-Time vs Per-Test Setup/TearDown + +`BeforeEverySetUpHook`/`AfterEverySetUpHook` and `BeforeEveryTearDownHook`/`AfterEveryTearDownHook` run for both per-test and one-time setup/teardown. Inside a hook, the supported way to distinguish the two is the current test context: `Context.Test.IsSuite` is `true` for [OneTimeSetUp]/[OneTimeTearDown] (suite context) and `false` for [SetUp]/[TearDown] (test method context). + +See [Example: One-Time vs Per-Test Setup and TearDown](#example-one-time-vs-per-test-setup-and-teardown) for a complete hook implementation. + +## Example: One-Time vs Per-Test Setup and TearDown + +[!code-csharp[ExecutionHookAttributeExample](~/snippets/Snippets.NUnit/ExecutionHookExamples.cs#OneTimeVsPerTestSetupTearDownExample)] + ## Example: Measure Time for Setup [!code-csharp[ExecutionHookAttributeExample](~/snippets/Snippets.NUnit/ExecutionHookExamples.cs#TimingHookAttribute)] diff --git a/docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs b/docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs index 41b903d15..a5a38dbbd 100644 --- a/docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs +++ b/docs/snippets/Snippets.NUnit/ExecutionHookExamples.cs @@ -22,7 +22,7 @@ public override void AfterEverySetUpHook(HookData hookData) if (_starts.TryGetValue(key, out var start)) { var elapsed = DateTime.UtcNow - start; - TestContext.WriteLine($"[Timing] " + + TestContext.Progress.WriteLine($"[Timing] " + $"{hookData.Context.Test.MethodName} " + $"took {elapsed.TotalMilliseconds:F1} ms"); } @@ -30,6 +30,24 @@ public override void AfterEverySetUpHook(HookData hookData) } #endregion + #region OneTimeVsPerTestSetupTearDownExample + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] + public sealed class OneTimeVsPerTestSetupTearDownHookAttribute : ExecutionHookAttribute + { + public override void BeforeEverySetUpHook(HookData data) + { + var scope = data.Context.Test.IsSuite ? "OneTimeSetUp" : "SetUp"; + TestContext.Progress.WriteLine($"{scope}: {data.HookedMethod?.Name}"); + } + + public override void AfterEveryTearDownHook(HookData data) + { + var scope = data.Context.Test.IsSuite ? "OneTimeTearDown" : "TearDown"; + TestContext.Progress.WriteLine($"{scope}: {data.HookedMethod?.Name}"); + } + } + #endregion + #region Usage [TestFixture] [TimeMeasurementHook] @@ -52,7 +70,7 @@ private void Log(string phase, HookData data, bool withException = false) var name = data.Context.Test.MethodName; var exInfo = withException && data.Exception != null ? $" (EX: {data.Exception.GetType().Name})" : string.Empty; - TestContext.WriteLine($"[{phase}] {name}{exInfo}"); + TestContext.Progress.WriteLine($"[{phase}] {name}{exInfo}"); } public override void BeforeEverySetUpHook(HookData d)