From 0cbec667414b442d8cd855235ef6d5d48a7f5147 Mon Sep 17 00:00:00 2001 From: Sean Killeen Date: Fri, 23 Oct 2026 13:57:00 -0400 Subject: [PATCH 1/3] Document SetUpFixture inheritance and sharing setup across projects Adds an Inheritance section to the SetUpFixture page covering the attribute being inherited, the concrete class's namespace setting the scope, and derived-class attribute precedence (TestFixture wins; Test-only derived classes become setup fixtures and their tests are not discovered). Adds a Sharing setup across projects section with the plain-class-plus-per-project-SetUpFixture pattern and the static Lazy alternative. All examples are executable snippets, and a verification test builds the assembly's test tree to confirm the precedence rules. Cross-links added from the OneTimeSetUp page and the setup guide. Closes #1293 Co-Authored-By: Claude Fable 5.1 --- .../writing-tests/attributes/onetimesetup.md | 3 +- .../writing-tests/attributes/setupfixture.md | 59 +++++ .../nunit/writing-tests/setup-and-teardown.md | 3 + .../SetUpFixtureAttributeExamples.cs | 244 ++++++++++++++++++ 4 files changed, 308 insertions(+), 1 deletion(-) diff --git a/docs/articles/nunit/writing-tests/attributes/onetimesetup.md b/docs/articles/nunit/writing-tests/attributes/onetimesetup.md index 71bc59a84..68280edec 100644 --- a/docs/articles/nunit/writing-tests/attributes/onetimesetup.md +++ b/docs/articles/nunit/writing-tests/attributes/onetimesetup.md @@ -34,7 +34,8 @@ Methods may be **async** (`Task` / `Task`): NUnit will wait for completion li If `OneTimeSetUp` lives on an **abstract or concrete base test class**, NUnit invokes it **for every derived fixture type** that inherits that hierarchy (along with setup on the descendants). When you truly need execution **exactly once for many unrelated fixtures**, prefer **`SetUpFixture`** or **`static`** initialization instead of repeating base-class -`OneTimeSetUp` semantics. +`OneTimeSetUp` semantics. See [Sharing setup across projects](xref:attribute-setupfixture#sharing-setup-across-projects) +for the pattern to use when the base class lives in a shared package. ## Failures and exceptions diff --git a/docs/articles/nunit/writing-tests/attributes/setupfixture.md b/docs/articles/nunit/writing-tests/attributes/setupfixture.md index 13fc76950..b10fa320a 100644 --- a/docs/articles/nunit/writing-tests/attributes/setupfixture.md +++ b/docs/articles/nunit/writing-tests/attributes/setupfixture.md @@ -67,6 +67,64 @@ The defined order is as follows... [Items 6 and 7 rarely come into play but the features are available for situations like code generation, where it may be more convenient to have multiple setup fixtures and/or methods.] +## Inheritance + +`[SetUpFixture]` is inherited. A class that derives from a class marked `[SetUpFixture]` is itself a setup fixture, +even if it does not repeat the attribute. This lets a shared test-support package provide the one-time setup and +teardown in a base class, with each test project deriving from it: + +[!code-csharp[SetUpFixtureInheritanceBase](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureInheritanceBase)] + +[!code-csharp[SetUpFixtureInheritanceDerived](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureInheritanceDerived)] + +NUnit applies these rules: + +* **The namespace of the concrete class sets the scope.** NUnit builds the setup fixture from the concrete class, so it + covers the namespace that class is in and the namespaces below it. The namespace of the base class plays no part. In + the example, the setup runs once for the tests in `SetUpFixtureInheritance.Consumer`. +* **An abstract base class is not a setup fixture by itself.** NUnit ignores abstract classes when it looks for + fixtures, so the base class does nothing until a test project derives from it. A base class in another assembly is + never scanned anyway, because NUnit only looks at the types in the test assembly. If the base class is concrete and + lives in the test assembly, it is a setup fixture for its own namespace in addition to the derived class. +* **Attributes on the derived class win.** NUnit looks for fixture attributes one level of the inheritance chain at a + time, starting at the concrete class, and uses the first level that has any. An inherited `[SetUpFixture]` is only + used when the derived class has no fixture attribute of its own: + * If the derived class is marked `[TestFixture]`, NUnit builds an ordinary test fixture and the inherited + `[SetUpFixture]` is ignored. Its tests run, and the base class `[OneTimeSetUp]` runs once for that fixture, like + any inherited `[OneTimeSetUp]`. + * If the derived class has `[Test]` methods but no `[TestFixture]`, the inherited `[SetUpFixture]` wins. NUnit builds + a setup fixture and the tests are **not discovered**. There is no warning; the tests are simply missing from the + run, and from `dotnet test --list-tests`. + +[!code-csharp[SetUpFixtureInheritancePrecedence](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureInheritancePrecedence)] + +> [!TIP] +> Put `[SetUpFixture]` on the concrete class as well. It is not required, but it makes the role of the class visible +> without looking at the base class, and it is easier to spot a `[Test]` method that ended up in the wrong place. + +## Sharing setup across projects + +When several test projects share one-time setup from a common package, a base class with `[SetUpFixture]` works, but +each project still has to declare a derived class. A `[OneTimeSetUp]` on a shared base *test* class is not an +alternative: NUnit runs it once for **every fixture** that derives from it, not once per run. See +[OneTimeSetUp](xref:attribute-onetimesetup#test-fixture-versus-setup-fixture). + +The simplest pattern keeps the package free of NUnit fixture attributes. The package provides a plain class with the +setup and teardown logic: + +[!code-csharp[SetUpFixtureSharingPackage](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureSharingPackage)] + +Each test project declares a small `[SetUpFixture]` that calls it. Placed outside any namespace, it runs once for the +whole project: + +[!code-csharp[SetUpFixtureSharingConsumer](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureSharingConsumer)] + +If you cannot ask consumers to add a class, start the resource on first use from a static `Lazy` in the base test +class. It starts once per test run, but NUnit offers no hook to stop it at the end of the run, so use this only for +resources the process can abandon: + +[!code-csharp[SetUpFixtureSharingLazy](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureSharingLazy)] + ## Notes 1. `SetUpFixture` classes must be public and have a default constructor (or be static). @@ -74,6 +132,7 @@ The defined order is as follows... 3. A setup fixture outside any namespace applies to the entire assembly. 4. Multiple setup fixtures at the same namespace level execute in indeterminate order. 5. Prior to NUnit 3.0, setup fixtures used `SetUp`/`TearDown`; NUnit 3+ uses `OneTimeSetUp`/`OneTimeTearDown`. +6. `SetUpFixture` is inherited. The namespace of the derived class sets the scope; see [Inheritance](#inheritance). ## See Also diff --git a/docs/articles/nunit/writing-tests/setup-and-teardown.md b/docs/articles/nunit/writing-tests/setup-and-teardown.md index 785b39acc..ed84450e8 100644 --- a/docs/articles/nunit/writing-tests/setup-and-teardown.md +++ b/docs/articles/nunit/writing-tests/setup-and-teardown.md @@ -58,6 +58,9 @@ child namespaces, and its `[OneTimeTearDown]` runs once after all of them. whole test assembly. - The class must be public and have a default constructor, or be static. - A setup fixture can only have `[OneTimeSetUp]` and `[OneTimeTearDown]` methods, not `[SetUp]` and `[TearDown]`. +- `[SetUpFixture]` is inherited, and the namespace of the derived class decides what it covers. See + [Inheritance](xref:attribute-setupfixture#inheritance) on the reference page, which also shows how to share the + setup from a package used by several test projects. ## Which one should I use? diff --git a/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs b/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs index d8b0c50ce..6751808bf 100644 --- a/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs +++ b/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs @@ -1,4 +1,8 @@ using NUnit.Framework; +using NUnit.Framework.Api; +using NUnit.Framework.Interfaces; + +#pragma warning disable CA1822 namespace Snippets.NUnit.Attributes.SampleNamespace { @@ -18,3 +22,243 @@ public void RunAfterAnyTests() } #endregion } + +namespace Snippets.NUnit.Attributes.SetUpFixtureInheritance.SharedPackage +{ + #region SetUpFixtureInheritanceBase + // Lives in a shared test-support package. NUnit skips abstract classes, + // so this class is not a setup fixture by itself. + [SetUpFixture] + public abstract class SharedEnvironmentSetUpFixture + { + public static bool IsStarted { get; private set; } + + [OneTimeSetUp] + public void StartEnvironment() + { + IsStarted = true; + } + + [OneTimeTearDown] + public void StopEnvironment() + { + IsStarted = false; + } + } + #endregion +} + +namespace Snippets.NUnit.Attributes.SetUpFixtureInheritance.Consumer +{ + using Snippets.NUnit.Attributes.SetUpFixtureInheritance.SharedPackage; + + #region SetUpFixtureInheritanceDerived + // Inherits [SetUpFixture] from the base class, so NUnit treats it as a setup + // fixture for the namespace of *this* class: ...SetUpFixtureInheritance.Consumer, + // not the namespace of the base class. + public class ConsumerEnvironment : SharedEnvironmentSetUpFixture + { + } + + public class ConsumerTests + { + [Test] + public void Environment_IsStartedByInheritedSetUpFixture() + { + Assert.That(SharedEnvironmentSetUpFixture.IsStarted, Is.True); + } + } + #endregion +} + +namespace Snippets.NUnit.Attributes.SetUpFixtureInheritance.Precedence +{ + using Snippets.NUnit.Attributes.SetUpFixtureInheritance.SharedPackage; + + #region SetUpFixtureInheritancePrecedence + // [TestFixture] on the derived class wins over the inherited [SetUpFixture]. + // This is an ordinary test fixture: its tests are discovered and run, and the + // base class's [OneTimeSetUp] runs once for this fixture, like any inherited + // [OneTimeSetUp]. + [TestFixture] + public class DerivedTestFixture : SharedEnvironmentSetUpFixture + { + [Test] + public void ThisTestIsDiscovered() + { + Assert.That(IsStarted, Is.True); + } + } + + // No fixture attribute on the derived class, so the inherited [SetUpFixture] + // wins. NUnit builds this class as a setup fixture for this namespace and + // silently ignores its [Test] methods. + public class DerivedWithTestsOnly : SharedEnvironmentSetUpFixture + { + [Test] + public void ThisTestIsNotDiscovered() + { + Assert.Fail("NUnit never runs this test."); + } + } + #endregion +} + +namespace Snippets.NUnit.Attributes.SetUpFixtureInheritance +{ + // Verifies the rules documented on the SetUpFixture page by building the test + // tree for this assembly the same way the NUnit framework does at discovery time. + public class SetUpFixtureInheritanceVerification + { + private static ITest BuildTestTree() + { + var builder = new DefaultTestAssemblyBuilder(); + return builder.Build(typeof(SetUpFixtureInheritanceVerification).Assembly, new Dictionary()); + } + + private static ITest? Find(ITest root, Type type) + { + if (root.TypeInfo?.Type == type) + return root; + + foreach (var child in root.Tests) + { + var found = Find(child, type); + if (found is not null) + return found; + } + + return null; + } + + [Test] + public void AbstractBaseClass_IsNotASetUpFixtureByItself() + { + var tree = BuildTestTree(); + + Assert.That(Find(tree, typeof(SharedPackage.SharedEnvironmentSetUpFixture)), Is.Null); + } + + [Test] + public void ConcreteDerivedClass_IsASetUpFixture_ForItsOwnNamespace() + { + var tree = BuildTestTree(); + + var fixture = Find(tree, typeof(Consumer.ConsumerEnvironment)); + Assert.That(fixture, Is.Not.Null); + Assert.That(fixture.TestType, Is.EqualTo("SetUpFixture")); + Assert.That(fixture.Parent?.FullName, Is.EqualTo("Snippets.NUnit.Attributes.SetUpFixtureInheritance")); + Assert.That(Find(fixture, typeof(Consumer.ConsumerTests)), Is.Not.Null); + } + + [Test] + public void DerivedClassWithTestFixtureAttribute_IsATestFixture() + { + var tree = BuildTestTree(); + + var fixture = Find(tree, typeof(Precedence.DerivedTestFixture)); + Assert.That(fixture, Is.Not.Null); + Assert.That(fixture.TestType, Is.EqualTo("TestFixture")); + Assert.That(fixture.TestCaseCount, Is.EqualTo(1)); + } + + [Test] + public void DerivedClassWithOnlyTestMethods_IsASetUpFixture_AndItsTestsAreNotDiscovered() + { + var tree = BuildTestTree(); + + var fixture = Find(tree, typeof(Precedence.DerivedWithTestsOnly)); + Assert.That(fixture, Is.Not.Null); + Assert.That(fixture.TestType, Is.EqualTo("SetUpFixture")); + Assert.That(fixture.Tests.Select(t => t.Name), Does.Not.Contain(nameof(Precedence.DerivedWithTestsOnly.ThisTestIsNotDiscovered))); + } + } +} + +namespace Snippets.NUnit.Attributes.SetUpFixtureSharing.SharedPackage +{ + #region SetUpFixtureSharingPackage + // In the shared package: a plain class with no NUnit attributes. + public static class SharedTestEnvironment + { + public static bool IsStarted { get; private set; } + + public static void Start() + { + // Start containers, seed the database, ... + IsStarted = true; + } + + public static void Stop() + { + IsStarted = false; + } + } + #endregion +} + +namespace Snippets.NUnit.Attributes.SetUpFixtureSharing.TestProject +{ + using Snippets.NUnit.Attributes.SetUpFixtureSharing.SharedPackage; + + #region SetUpFixtureSharingConsumer + // In each test project that uses the package. Put it outside any namespace + // to cover the whole project. + [SetUpFixture] + public class SharedTestEnvironmentSetUp + { + [OneTimeSetUp] + public void Start() + { + SharedTestEnvironment.Start(); + } + + [OneTimeTearDown] + public void Stop() + { + SharedTestEnvironment.Stop(); + } + } + + public class OrderTests + { + [Test] + public void Environment_IsStartedOnceForTheProject() + { + Assert.That(SharedTestEnvironment.IsStarted, Is.True); + } + } + #endregion +} + +namespace Snippets.NUnit.Attributes.SetUpFixtureSharing.LazyAlternative +{ + public sealed class TestServer + { + public static TestServer Start() => new(); + + public string BaseAddress => "http://localhost:5000"; + } + + #region SetUpFixtureSharingLazy + // In the shared package: start the resource the first time any test needs it. + // Lazy guarantees a single start per test run, but nothing stops the + // resource at the end of the run. + public abstract class SharedServerTestBase + { + private static readonly Lazy Server = new(TestServer.Start); + + protected static TestServer SharedServer => Server.Value; + } + + // In a test project: no setup fixture needed. + public class CustomerTests : SharedServerTestBase + { + [Test] + public void Server_IsAvailable() + { + Assert.That(SharedServer.BaseAddress, Does.StartWith("http://")); + } + } + #endregion +} From 4c9996000ed42dd61e3848eed61f2194a7dd0b6f Mon Sep 17 00:00:00 2001 From: Sean Killeen Date: Fri, 23 Oct 2026 14:40:28 -0400 Subject: [PATCH 2/3] Say SetUpFixture can be inherited rather than is inherited Co-Authored-By: Claude Fable 5.1 --- .../articles/nunit/writing-tests/attributes/setupfixture.md | 6 +++--- docs/articles/nunit/writing-tests/setup-and-teardown.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/articles/nunit/writing-tests/attributes/setupfixture.md b/docs/articles/nunit/writing-tests/attributes/setupfixture.md index b10fa320a..93852cc01 100644 --- a/docs/articles/nunit/writing-tests/attributes/setupfixture.md +++ b/docs/articles/nunit/writing-tests/attributes/setupfixture.md @@ -69,8 +69,8 @@ The defined order is as follows... ## Inheritance -`[SetUpFixture]` is inherited. A class that derives from a class marked `[SetUpFixture]` is itself a setup fixture, -even if it does not repeat the attribute. This lets a shared test-support package provide the one-time setup and +`[SetUpFixture]` can be inherited. A class that derives from a class marked `[SetUpFixture]` is itself a setup +fixture, even if it does not repeat the attribute. This lets a shared test-support package provide the one-time setup and teardown in a base class, with each test project deriving from it: [!code-csharp[SetUpFixtureInheritanceBase](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureInheritanceBase)] @@ -132,7 +132,7 @@ resources the process can abandon: 3. A setup fixture outside any namespace applies to the entire assembly. 4. Multiple setup fixtures at the same namespace level execute in indeterminate order. 5. Prior to NUnit 3.0, setup fixtures used `SetUp`/`TearDown`; NUnit 3+ uses `OneTimeSetUp`/`OneTimeTearDown`. -6. `SetUpFixture` is inherited. The namespace of the derived class sets the scope; see [Inheritance](#inheritance). +6. `SetUpFixture` can be inherited. The namespace of the derived class sets the scope; see [Inheritance](#inheritance). ## See Also diff --git a/docs/articles/nunit/writing-tests/setup-and-teardown.md b/docs/articles/nunit/writing-tests/setup-and-teardown.md index ed84450e8..c0ec6f148 100644 --- a/docs/articles/nunit/writing-tests/setup-and-teardown.md +++ b/docs/articles/nunit/writing-tests/setup-and-teardown.md @@ -58,7 +58,7 @@ child namespaces, and its `[OneTimeTearDown]` runs once after all of them. whole test assembly. - The class must be public and have a default constructor, or be static. - A setup fixture can only have `[OneTimeSetUp]` and `[OneTimeTearDown]` methods, not `[SetUp]` and `[TearDown]`. -- `[SetUpFixture]` is inherited, and the namespace of the derived class decides what it covers. See +- `[SetUpFixture]` can be inherited, and the namespace of the derived class decides what it covers. See [Inheritance](xref:attribute-setupfixture#inheritance) on the reference page, which also shows how to share the setup from a package used by several test projects. From 61dc90195416e6c70b465a4d9ba6e9ae8c695a73 Mon Sep 17 00:00:00 2001 From: Sean Killeen Date: Fri, 23 Oct 2026 14:42:04 -0400 Subject: [PATCH 3/3] Describe the static Lazy lifetime as per loaded type, not per run Addresses Copilot review feedback on PR #1298. Co-Authored-By: Claude Fable 5.1 --- docs/articles/nunit/writing-tests/attributes/setupfixture.md | 5 +++-- .../Attributes/SetUpFixtureAttributeExamples.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/articles/nunit/writing-tests/attributes/setupfixture.md b/docs/articles/nunit/writing-tests/attributes/setupfixture.md index 93852cc01..2e6d8023d 100644 --- a/docs/articles/nunit/writing-tests/attributes/setupfixture.md +++ b/docs/articles/nunit/writing-tests/attributes/setupfixture.md @@ -120,8 +120,9 @@ whole project: [!code-csharp[SetUpFixtureSharingConsumer](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureSharingConsumer)] If you cannot ask consumers to add a class, start the resource on first use from a static `Lazy` in the base test -class. It starts once per test run, but NUnit offers no hook to stop it at the end of the run, so use this only for -resources the process can abandon: +class. The resource lives as long as the type stays loaded: `dotnet test` starts a fresh test host per run, so each +test project gets its own instance, but a host that keeps the assembly loaded between runs reuses it. NUnit offers no +hook to stop it at the end of the run, so use this only for resources the process can abandon: [!code-csharp[SetUpFixtureSharingLazy](~/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs#SetUpFixtureSharingLazy)] diff --git a/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs b/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs index 6751808bf..0de194af9 100644 --- a/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs +++ b/docs/snippets/Snippets.NUnit/Attributes/SetUpFixtureAttributeExamples.cs @@ -242,8 +242,8 @@ public sealed class TestServer #region SetUpFixtureSharingLazy // In the shared package: start the resource the first time any test needs it. - // Lazy guarantees a single start per test run, but nothing stops the - // resource at the end of the run. + // Lazy starts it once per process that loads this type, and nothing stops + // it at the end of the run. public abstract class SharedServerTestBase { private static readonly Lazy Server = new(TestServer.Start);