From 71ac045bd7f81caa95779af7b512978406099475 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 29 Sep 2026 14:12:37 +1000 Subject: [PATCH 1/2] Add [NoDb] attribute to opt a test out of a database Tests in a LocalDbTestBase class that do not touch the database (rendering, formatting, validation) currently get a new database per test by default, the most expensive mode. [NoDb] on a method or class skips building one. Database is null, and ArrangeData, ActData, AssertData and Reset throw. Supported in the MSTest, NUnit, TUnit and xunit.v3 packages. Bump to 27.2.0. --- pages/choosing-a-test-strategy.md | 1 + pages/ef-mstest-usage.md | 15 +++- pages/ef-nunit-usage.md | 15 +++- pages/ef-tunit-usage.md | 15 +++- pages/ef-xunitv3-usage.md | 15 +++- .../choosing-a-test-strategy.source.md | 1 + pages/mdsource/ef-mstest-usage.source.md | 5 ++ pages/mdsource/ef-nunit-usage.source.md | 5 ++ pages/mdsource/ef-tunit-usage.source.md | 5 ++ pages/mdsource/ef-xunitv3-usage.source.md | 5 ++ pages/mdsource/no-db.include.md | 7 ++ pages/mdsource/pooled-db.include.md | 2 +- pages/mdsource/shared-db.include.md | 2 +- src/Directory.Build.props | 2 +- src/EfLocalDb.MSTest.Tests/NoDbTests.cs | 39 +++++++++++ .../PooledAndSharedDbTests.cs | 2 +- src/EfLocalDb.MSTest/LocalDbTestBase.cs | 21 +++++- src/EfLocalDb.MSTest/NoDbAttribute.cs | 14 ++++ src/EfLocalDb.NUnit.Tests/NoDbTests.cs | 41 +++++++++++ .../PooledAndSharedDbTests.cs | 2 +- src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj | 1 + src/EfLocalDb.NUnit/LocalDbTestBase.cs | 21 +++++- src/EfLocalDb.TUnit.Tests/NoDbTests.cs | 37 ++++++++++ .../PooledAndSharedDbTests.cs | 2 +- src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj | 1 + src/EfLocalDb.TUnit/LocalDbTestBase.cs | 21 +++++- src/EfLocalDb.Tests/DbAttributeReaderTests.cs | 55 +++++++++++++-- src/EfLocalDb.Xunit.V3.Tests/NoDbTests.cs | 37 ++++++++++ .../PooledAndSharedDbTests.cs | 2 +- .../EfLocalDb.Xunit.V3.csproj | 1 + src/EfLocalDb.Xunit.V3/LocalDbTestBase.cs | 21 +++++- src/EfLocalDb/DbAttributeReader.cs | 69 ++++++++++--------- src/EfLocalDb/DbMode.cs | 3 +- 33 files changed, 427 insertions(+), 58 deletions(-) create mode 100644 pages/mdsource/no-db.include.md create mode 100644 src/EfLocalDb.MSTest.Tests/NoDbTests.cs create mode 100644 src/EfLocalDb.MSTest/NoDbAttribute.cs create mode 100644 src/EfLocalDb.NUnit.Tests/NoDbTests.cs create mode 100644 src/EfLocalDb.TUnit.Tests/NoDbTests.cs create mode 100644 src/EfLocalDb.Xunit.V3.Tests/NoDbTests.cs diff --git a/pages/choosing-a-test-strategy.md b/pages/choosing-a-test-strategy.md index afafe335..895ab003 100644 --- a/pages/choosing-a-test-strategy.md +++ b/pages/choosing-a-test-strategy.md @@ -76,6 +76,7 @@ A common split: * Mocks or fakes for domain and business logic. * LocalDb for data access, queries, migrations, and end-to-end integration tests. * `[PooledDb]` or `[SharedDb]` for large sets of read-only tests, with a new database per test for anything that writes. + * `[NoDb]` for tests in a `LocalDbTestBase` class that do not touch the database, such as rendering or validation tests. This keeps them next to related database tests without paying for a database. ## Summary diff --git a/pages/ef-mstest-usage.md b/pages/ef-mstest-usage.md index 3addc434..c3a2ad39 100644 --- a/pages/ef-mstest-usage.md +++ b/pages/ef-mstest-usage.md @@ -648,7 +648,7 @@ public async Task VerifyEntity_Queryable() Mark test methods with `[SharedDb]` to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data. -`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. The shared database is read-only and any write throws, not only `SaveChanges`: `ExecuteUpdate`, `ExecuteDelete`, `ExecuteSqlRaw` and hand-written commands are blocked too. Tests that need to write should use `[PooledDb]` instead. @@ -675,7 +675,7 @@ public class SharedDbTests : LocalDbTestBase Mark test methods with `[PooledDb]` to lease a database from a fixed pool instead of creating one per test. The pool is built once from the template, and each test leases a database for its duration, writes inside a transaction, and rolls that transaction back on release so the next test sees the template state again. -`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. Two costs disappear. The per-test file copy and attach is gone, and — usually the larger one — so is repeated query plan compilation: SQL Server keys the plan cache by database, so a database per test means every query is compiled afresh for every test and no plan is ever reused. A small pool lets those plans be reused for the rest of the run. @@ -759,6 +759,17 @@ public class PooledDbTests : LocalDbTestBase +## NoDb + +Mark a test method with `[NoDb]` when it does not touch the database, for example a test that renders a document, formats output, or runs a validator. The test still inherits from `LocalDbTestBase`, so it can live next to related database tests, but no database is built for it. + +A test without any attribute gets a new database per test, which is the most expensive mode. So a database-free test in a `LocalDbTestBase` class should be marked `[NoDb]` rather than left unmarked. + +In a `[NoDb]` test `Database` is null, and accessing `ArrangeData`, `ActData`, or `AssertData`, or calling `Reset`, throws. + +`[NoDb]` can also be applied to a test class. The nearest attribute wins, so a method marked `[PooledDb]`, `[SharedDb]`, or `[NewDb]` in a `[NoDb]` class gets a database. Applying `[NoDb]` together with `[PooledDb]`, `[SharedDb]` or `[NewDb]` to the same method or class throws. + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/ef-nunit-usage.md b/pages/ef-nunit-usage.md index b8bc1b4e..13156552 100644 --- a/pages/ef-nunit-usage.md +++ b/pages/ef-nunit-usage.md @@ -657,7 +657,7 @@ public async Task VerifyEntity_Queryable() Mark test methods with `[SharedDb]` to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data. -`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. The shared database is read-only and any write throws, not only `SaveChanges`: `ExecuteUpdate`, `ExecuteDelete`, `ExecuteSqlRaw` and hand-written commands are blocked too. Tests that need to write should use `[PooledDb]` instead. @@ -685,7 +685,7 @@ public class SharedDbTests : Mark test methods with `[PooledDb]` to lease a database from a fixed pool instead of creating one per test. The pool is built once from the template, and each test leases a database for its duration, writes inside a transaction, and rolls that transaction back on release so the next test sees the template state again. -`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. Two costs disappear. The per-test file copy and attach is gone, and — usually the larger one — so is repeated query plan compilation: SQL Server keys the plan cache by database, so a database per test means every query is compiled afresh for every test and no plan is ever reused. A small pool lets those plans be reused for the rest of the run. @@ -770,6 +770,17 @@ public class PooledDbTests : +## NoDb + +Mark a test method with `[NoDb]` when it does not touch the database, for example a test that renders a document, formats output, or runs a validator. The test still inherits from `LocalDbTestBase`, so it can live next to related database tests, but no database is built for it. + +A test without any attribute gets a new database per test, which is the most expensive mode. So a database-free test in a `LocalDbTestBase` class should be marked `[NoDb]` rather than left unmarked. + +In a `[NoDb]` test `Database` is null, and accessing `ArrangeData`, `ActData`, or `AssertData`, or calling `Reset`, throws. + +`[NoDb]` can also be applied to a test class. The nearest attribute wins, so a method marked `[PooledDb]`, `[SharedDb]`, or `[NewDb]` in a `[NoDb]` class gets a database. Applying `[NoDb]` together with `[PooledDb]`, `[SharedDb]` or `[NewDb]` to the same method or class throws. + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/ef-tunit-usage.md b/pages/ef-tunit-usage.md index 3c857f7c..7fa3c101 100644 --- a/pages/ef-tunit-usage.md +++ b/pages/ef-tunit-usage.md @@ -656,7 +656,7 @@ public async Task VerifyEntity_Queryable() Mark test methods with `[SharedDb]` to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data. -`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. The shared database is read-only and any write throws, not only `SaveChanges`: `ExecuteUpdate`, `ExecuteDelete`, `ExecuteSqlRaw` and hand-written commands are blocked too. Tests that need to write should use `[PooledDb]` instead. @@ -682,7 +682,7 @@ public class SharedDbTests : LocalDbTestBase Mark test methods with `[PooledDb]` to lease a database from a fixed pool instead of creating one per test. The pool is built once from the template, and each test leases a database for its duration, writes inside a transaction, and rolls that transaction back on release so the next test sees the template state again. -`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. Two costs disappear. The per-test file copy and attach is gone, and — usually the larger one — so is repeated query plan compilation: SQL Server keys the plan cache by database, so a database per test means every query is compiled afresh for every test and no plan is ever reused. A small pool lets those plans be reused for the rest of the run. @@ -766,6 +766,17 @@ public class PooledDbTests : LocalDbTestBase +## NoDb + +Mark a test method with `[NoDb]` when it does not touch the database, for example a test that renders a document, formats output, or runs a validator. The test still inherits from `LocalDbTestBase`, so it can live next to related database tests, but no database is built for it. + +A test without any attribute gets a new database per test, which is the most expensive mode. So a database-free test in a `LocalDbTestBase` class should be marked `[NoDb]` rather than left unmarked. + +In a `[NoDb]` test `Database` is null, and accessing `ArrangeData`, `ActData`, or `AssertData`, or calling `Reset`, throws. + +`[NoDb]` can also be applied to a test class. The nearest attribute wins, so a method marked `[PooledDb]`, `[SharedDb]`, or `[NewDb]` in a `[NoDb]` class gets a database. Applying `[NoDb]` together with `[PooledDb]`, `[SharedDb]` or `[NewDb]` to the same method or class throws. + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/ef-xunitv3-usage.md b/pages/ef-xunitv3-usage.md index 5d10f086..19ba64f0 100644 --- a/pages/ef-xunitv3-usage.md +++ b/pages/ef-xunitv3-usage.md @@ -655,7 +655,7 @@ public async Task VerifyEntity_Queryable() Mark test methods with `[SharedDb]` to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data. -`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. The shared database is read-only and any write throws, not only `SaveChanges`: `ExecuteUpdate`, `ExecuteDelete`, `ExecuteSqlRaw` and hand-written commands are blocked too. Tests that need to write should use `[PooledDb]` instead. @@ -681,7 +681,7 @@ public class SharedDbTests : LocalDbTestBase Mark test methods with `[PooledDb]` to lease a database from a fixed pool instead of creating one per test. The pool is built once from the template, and each test leases a database for its duration, writes inside a transaction, and rolls that transaction back on release so the next test sees the template state again. -`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. Two costs disappear. The per-test file copy and attach is gone, and — usually the larger one — so is repeated query plan compilation: SQL Server keys the plan cache by database, so a database per test means every query is compiled afresh for every test and no plan is ever reused. A small pool lets those plans be reused for the rest of the run. @@ -764,6 +764,17 @@ public class PooledDbTests : LocalDbTestBase +## NoDb + +Mark a test method with `[NoDb]` when it does not touch the database, for example a test that renders a document, formats output, or runs a validator. The test still inherits from `LocalDbTestBase`, so it can live next to related database tests, but no database is built for it. + +A test without any attribute gets a new database per test, which is the most expensive mode. So a database-free test in a `LocalDbTestBase` class should be marked `[NoDb]` rather than left unmarked. + +In a `[NoDb]` test `Database` is null, and accessing `ArrangeData`, `ActData`, or `AssertData`, or calling `Reset`, throws. + +`[NoDb]` can also be applied to a test class. The nearest attribute wins, so a method marked `[PooledDb]`, `[SharedDb]`, or `[NewDb]` in a `[NoDb]` class gets a database. Applying `[NoDb]` together with `[PooledDb]`, `[SharedDb]` or `[NewDb]` to the same method or class throws. + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/mdsource/choosing-a-test-strategy.source.md b/pages/mdsource/choosing-a-test-strategy.source.md index 38c98225..d4151ab5 100644 --- a/pages/mdsource/choosing-a-test-strategy.source.md +++ b/pages/mdsource/choosing-a-test-strategy.source.md @@ -69,6 +69,7 @@ A common split: * Mocks or fakes for domain and business logic. * LocalDb for data access, queries, migrations, and end-to-end integration tests. * `[PooledDb]` or `[SharedDb]` for large sets of read-only tests, with a new database per test for anything that writes. + * `[NoDb]` for tests in a `LocalDbTestBase` class that do not touch the database, such as rendering or validation tests. This keeps them next to related database tests without paying for a database. ## Summary diff --git a/pages/mdsource/ef-mstest-usage.source.md b/pages/mdsource/ef-mstest-usage.source.md index eddf30c0..d3d2f341 100644 --- a/pages/mdsource/ef-mstest-usage.source.md +++ b/pages/mdsource/ef-mstest-usage.source.md @@ -83,6 +83,11 @@ include: pooled-db snippet: PooledDbTestsMSTest +## NoDb + +include: no-db + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/mdsource/ef-nunit-usage.source.md b/pages/mdsource/ef-nunit-usage.source.md index d8738281..572531c3 100644 --- a/pages/mdsource/ef-nunit-usage.source.md +++ b/pages/mdsource/ef-nunit-usage.source.md @@ -83,6 +83,11 @@ include: pooled-db snippet: PooledDbTestsNUnit +## NoDb + +include: no-db + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/mdsource/ef-tunit-usage.source.md b/pages/mdsource/ef-tunit-usage.source.md index 10fa6774..7e34ed19 100644 --- a/pages/mdsource/ef-tunit-usage.source.md +++ b/pages/mdsource/ef-tunit-usage.source.md @@ -83,6 +83,11 @@ include: pooled-db snippet: PooledDbTestsTUnit +## NoDb + +include: no-db + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/mdsource/ef-xunitv3-usage.source.md b/pages/mdsource/ef-xunitv3-usage.source.md index d61d670b..c0987f34 100644 --- a/pages/mdsource/ef-xunitv3-usage.source.md +++ b/pages/mdsource/ef-xunitv3-usage.source.md @@ -83,6 +83,11 @@ include: pooled-db snippet: PooledDbTestsXunitV3 +## NoDb + +include: no-db + + ## Parallel Execution To run tests in parallel, configure parallelism at the assembly level: diff --git a/pages/mdsource/no-db.include.md b/pages/mdsource/no-db.include.md new file mode 100644 index 00000000..d4bf9a7c --- /dev/null +++ b/pages/mdsource/no-db.include.md @@ -0,0 +1,7 @@ +Mark a test method with `[NoDb]` when it does not touch the database, for example a test that renders a document, formats output, or runs a validator. The test still inherits from `LocalDbTestBase`, so it can live next to related database tests, but no database is built for it. + +A test without any attribute gets a new database per test, which is the most expensive mode. So a database-free test in a `LocalDbTestBase` class should be marked `[NoDb]` rather than left unmarked. + +In a `[NoDb]` test `Database` is null, and accessing `ArrangeData`, `ActData`, or `AssertData`, or calling `Reset`, throws. + +`[NoDb]` can also be applied to a test class. The nearest attribute wins, so a method marked `[PooledDb]`, `[SharedDb]`, or `[NewDb]` in a `[NoDb]` class gets a database. Applying `[NoDb]` together with `[PooledDb]`, `[SharedDb]` or `[NewDb]` to the same method or class throws. diff --git a/pages/mdsource/pooled-db.include.md b/pages/mdsource/pooled-db.include.md index 6f254a2b..9e20036b 100644 --- a/pages/mdsource/pooled-db.include.md +++ b/pages/mdsource/pooled-db.include.md @@ -1,6 +1,6 @@ Mark test methods with `[PooledDb]` to lease a database from a fixed pool instead of creating one per test. The pool is built once from the template, and each test leases a database for its duration, writes inside a transaction, and rolls that transaction back on release so the next test sees the template state again. -`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[PooledDb]` can also be applied to a test class, or to the assembly with `[assembly: PooledDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. Two costs disappear. The per-test file copy and attach is gone, and — usually the larger one — so is repeated query plan compilation: SQL Server keys the plan cache by database, so a database per test means every query is compiled afresh for every test and no plan is ever reused. A small pool lets those plans be reused for the rest of the run. diff --git a/pages/mdsource/shared-db.include.md b/pages/mdsource/shared-db.include.md index 28fa790b..5b19b4a4 100644 --- a/pages/mdsource/shared-db.include.md +++ b/pages/mdsource/shared-db.include.md @@ -1,5 +1,5 @@ Mark test methods with `[SharedDb]` to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data. -`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]` and `[NewDb]` to the same method, class, or assembly throws. +`[SharedDb]` can also be applied to a test class, or to the assembly with `[assembly: SharedDb]`. The nearest attribute wins: a method attribute overrides a class attribute, which overrides an assembly attribute. Mark a test method or class with `[NewDb]` to opt it out and create a database per test. Applying more than one of `[PooledDb]`, `[SharedDb]`, `[NewDb]` and `[NoDb]` to the same method, class, or assembly throws. The shared database is read-only and any write throws, not only `SaveChanges`: `ExecuteUpdate`, `ExecuteDelete`, `ExecuteSqlRaw` and hand-written commands are blocked too. Tests that need to write should use `[PooledDb]` instead. diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 919db55c..a9d313be 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ CS1591;CA1416;CS8632;NU1608;NU1109 - 27.1.0 + 27.2.0 preview 1.0.0 false diff --git a/src/EfLocalDb.MSTest.Tests/NoDbTests.cs b/src/EfLocalDb.MSTest.Tests/NoDbTests.cs new file mode 100644 index 00000000..7b352b23 --- /dev/null +++ b/src/EfLocalDb.MSTest.Tests/NoDbTests.cs @@ -0,0 +1,39 @@ +[TestClass] +public class NoDbTests : LocalDbTestBase +{ + [TestMethod] + [NoDb] + public void HasNoDatabase() => + Assert.IsNull(Database); + + [TestMethod] + [NoDb] + public void ArrangeDataThrows() + { + var exception = Assert.ThrowsExactly(() => _ = ArrangeData); + Assert.Contains("[NoDb]", exception.Message); + } + + [TestMethod] + [NoDb] + public async Task ResetThrows() => + await Assert.ThrowsExactlyAsync(Reset); + + [TestMethod] + public void UnmarkedMethodHasDatabase() => + Assert.IsNotNull(Database); +} + +[TestClass] +[NoDb] +public class ClassLevelNoDbTests : LocalDbTestBase +{ + [TestMethod] + public void HasNoDatabase() => + Assert.IsNull(Database); + + [TestMethod] + [PooledDb] + public void MethodOverridesClass() => + Assert.IsNotNull(Database.Transaction); +} diff --git a/src/EfLocalDb.MSTest.Tests/PooledAndSharedDbTests.cs b/src/EfLocalDb.MSTest.Tests/PooledAndSharedDbTests.cs index ed109577..77b7ba86 100644 --- a/src/EfLocalDb.MSTest.Tests/PooledAndSharedDbTests.cs +++ b/src/EfLocalDb.MSTest.Tests/PooledAndSharedDbTests.cs @@ -11,6 +11,6 @@ public class PooledAndSharedDbTests : LocalDbTestBase public async Task Throws() { var exception = await Assert.ThrowsExactlyAsync(() => base.SetUp()); - Assert.AreEqual("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test method.", exception.Message); + Assert.AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method.", exception.Message); } } diff --git a/src/EfLocalDb.MSTest/LocalDbTestBase.cs b/src/EfLocalDb.MSTest/LocalDbTestBase.cs index 83aa9db8..d71930fe 100644 --- a/src/EfLocalDb.MSTest/LocalDbTestBase.cs +++ b/src/EfLocalDb.MSTest/LocalDbTestBase.cs @@ -13,6 +13,7 @@ public abstract partial class LocalDbTestBase : bool isSharedDb; bool isPooledDb; + bool isNoDb; public static void Initialize( ConstructInstance? constructInstance = null, @@ -53,16 +54,23 @@ public virtual Task SetUp() var methodInfo = GetType() .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .First(_ => _.Name == TestContext.TestName && !_.IsGenericMethod); - var mode = DbAttributeReader.Read(methodInfo, GetType()); + var mode = DbAttributeReader.Read(methodInfo, GetType()); isSharedDb = mode == DbMode.Shared; isPooledDb = mode == DbMode.Pooled; + isNoDb = mode == DbMode.None; QueryFilter.Enable(); + if (isNoDb) + { + return Task.CompletedTask; + } + return Reset(); } public async Task Reset() { + ThrowIfNoDb(); phase = Phase.Arrange; var type = GetType().FullName!; var member = TestContext.TestName; @@ -139,10 +147,19 @@ static Storage GetStorage(Assembly callingAssembly) => public SqlDatabase Database { get; private set; } = null!; + void ThrowIfNoDb() + { + if (isNoDb) + { + throw new("The test is marked [NoDb], so it has no database. Remove [NoDb] to use ArrangeData, ActData, AssertData, or Reset."); + } + } + public virtual T ArrangeData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { throw new("Phase has already moved to Act. Check for a ActData usage in the preceding code."); @@ -161,6 +178,7 @@ public virtual T ActData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { return actData; @@ -182,6 +200,7 @@ public virtual T AssertData { get { + ThrowIfNoDb(); if (phase == Phase.Assert) { return Database.NoTrackingContext; diff --git a/src/EfLocalDb.MSTest/NoDbAttribute.cs b/src/EfLocalDb.MSTest/NoDbAttribute.cs new file mode 100644 index 00000000..5636ffc5 --- /dev/null +++ b/src/EfLocalDb.MSTest/NoDbAttribute.cs @@ -0,0 +1,14 @@ +namespace EfLocalDb; + +/// +/// Runs the test without a database. Use for tests in a LocalDbTestBase class that do not touch +/// the database, such as rendering, formatting, or validation tests, so they skip the cost of +/// building one. In such a test Database is null, and accessing ArrangeData, +/// ActData, or AssertData, or calling Reset, throws. +/// +/// Can be applied to a test method or a test class. The nearest wins, so a method marked +/// in a class uses a pooled database. +/// +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public sealed class NoDbAttribute : Attribute; diff --git a/src/EfLocalDb.NUnit.Tests/NoDbTests.cs b/src/EfLocalDb.NUnit.Tests/NoDbTests.cs new file mode 100644 index 00000000..d917f1e6 --- /dev/null +++ b/src/EfLocalDb.NUnit.Tests/NoDbTests.cs @@ -0,0 +1,41 @@ +[TestFixture] +public class NoDbTests : + LocalDbTestBase +{ + [Test] + [NoDb] + public void HasNoDatabase() => + IsNull(Database); + + [Test] + [NoDb] + public void ArrangeDataThrows() + { + var exception = Throws(() => _ = ArrangeData)!; + That(exception.Message, Does.Contain("[NoDb]")); + } + + [Test] + [NoDb] + public void ResetThrows() => + ThrowsAsync(Reset); + + [Test] + public void UnmarkedMethodHasDatabase() => + IsNotNull(Database); +} + +[TestFixture] +[NoDb] +public class ClassLevelNoDbTests : + LocalDbTestBase +{ + [Test] + public void HasNoDatabase() => + IsNull(Database); + + [Test] + [PooledDb] + public void MethodOverridesClass() => + IsNotNull(Database.Transaction); +} diff --git a/src/EfLocalDb.NUnit.Tests/PooledAndSharedDbTests.cs b/src/EfLocalDb.NUnit.Tests/PooledAndSharedDbTests.cs index 711299c7..d3683abe 100644 --- a/src/EfLocalDb.NUnit.Tests/PooledAndSharedDbTests.cs +++ b/src/EfLocalDb.NUnit.Tests/PooledAndSharedDbTests.cs @@ -12,6 +12,6 @@ public class PooledAndSharedDbTests : public async Task Throws() { var exception = (await ThrowsAsync(() => base.SetUp()))!; - AreEqual("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test method.", exception.Message); + AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method.", exception.Message); } } diff --git a/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj b/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj index 09f323f1..1edc1f19 100644 --- a/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj +++ b/src/EfLocalDb.NUnit/EfLocalDb.NUnit.csproj @@ -12,6 +12,7 @@ + diff --git a/src/EfLocalDb.NUnit/LocalDbTestBase.cs b/src/EfLocalDb.NUnit/LocalDbTestBase.cs index e25afff8..2116662d 100644 --- a/src/EfLocalDb.NUnit/LocalDbTestBase.cs +++ b/src/EfLocalDb.NUnit/LocalDbTestBase.cs @@ -13,6 +13,7 @@ public abstract partial class LocalDbTestBase : bool isSharedDb; bool isPooledDb; + bool isNoDb; public static void Initialize( ConstructInstance? constructInstance = null, @@ -54,16 +55,23 @@ public virtual Task SetUp() #pragma warning disable CS0618 // Type or member is obsolete var methodInfo = test.Method!.MethodInfo; #pragma warning restore CS0618 // Type or member is obsolete - var mode = DbAttributeReader.Read(methodInfo, GetType()); + var mode = DbAttributeReader.Read(methodInfo, GetType()); isSharedDb = mode == DbMode.Shared; isPooledDb = mode == DbMode.Pooled; + isNoDb = mode == DbMode.None; QueryFilter.Enable(); + if (isNoDb) + { + return Task.CompletedTask; + } + return Reset(); } public async Task Reset() { + ThrowIfNoDb(); phase = Phase.Arrange; var test = TestContext.CurrentContext.Test; var type = test.ClassName!; @@ -141,10 +149,19 @@ static Storage GetStorage(Assembly callingAssembly) => public SqlDatabase Database { get; private set; } = null!; + void ThrowIfNoDb() + { + if (isNoDb) + { + throw new("The test is marked [NoDb], so it has no database. Remove [NoDb] to use ArrangeData, ActData, AssertData, or Reset."); + } + } + public virtual T ArrangeData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { throw new("Phase has already moved to Act. Check for a ActData usage in the preceding code."); @@ -163,6 +180,7 @@ public virtual T ActData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { return actData; @@ -184,6 +202,7 @@ public virtual T AssertData { get { + ThrowIfNoDb(); if (phase == Phase.Assert) { return Database.NoTrackingContext; diff --git a/src/EfLocalDb.TUnit.Tests/NoDbTests.cs b/src/EfLocalDb.TUnit.Tests/NoDbTests.cs new file mode 100644 index 00000000..6368c5f8 --- /dev/null +++ b/src/EfLocalDb.TUnit.Tests/NoDbTests.cs @@ -0,0 +1,37 @@ +public class NoDbTests : LocalDbTestBase +{ + [Test] + [NoDb] + public async Task HasNoDatabase() => + await Assert.That(Database).IsNull(); + + [Test] + [NoDb] + public async Task ArrangeDataThrows() + { + var exception = await Assert.That(() => _ = ArrangeData).Throws(); + await Assert.That(exception!.Message).Contains("[NoDb]"); + } + + [Test] + [NoDb] + public async Task ResetThrows() => + await Assert.That(Reset).Throws(); + + [Test] + public async Task UnmarkedMethodHasDatabase() => + await Assert.That(Database).IsNotNull(); +} + +[NoDb] +public class ClassLevelNoDbTests : LocalDbTestBase +{ + [Test] + public async Task HasNoDatabase() => + await Assert.That(Database).IsNull(); + + [Test] + [PooledDb] + public async Task MethodOverridesClass() => + await Assert.That(Database.Transaction).IsNotNull(); +} diff --git a/src/EfLocalDb.TUnit.Tests/PooledAndSharedDbTests.cs b/src/EfLocalDb.TUnit.Tests/PooledAndSharedDbTests.cs index ad8bc2cc..955888c4 100644 --- a/src/EfLocalDb.TUnit.Tests/PooledAndSharedDbTests.cs +++ b/src/EfLocalDb.TUnit.Tests/PooledAndSharedDbTests.cs @@ -11,6 +11,6 @@ public async Task Throws() { var exception = (await Assert.ThrowsExactlyAsync(() => base.SetUp()))!; await Assert.That(exception.Message) - .IsEqualTo("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test method."); + .IsEqualTo("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method."); } } diff --git a/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj b/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj index 801fd727..80629127 100644 --- a/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj +++ b/src/EfLocalDb.TUnit/EfLocalDb.TUnit.csproj @@ -12,6 +12,7 @@ + diff --git a/src/EfLocalDb.TUnit/LocalDbTestBase.cs b/src/EfLocalDb.TUnit/LocalDbTestBase.cs index 1667391d..a0234d02 100644 --- a/src/EfLocalDb.TUnit/LocalDbTestBase.cs +++ b/src/EfLocalDb.TUnit/LocalDbTestBase.cs @@ -11,6 +11,7 @@ public abstract partial class LocalDbTestBase : bool isSharedDb; bool isPooledDb; + bool isNoDb; public static void Initialize( ConstructInstance? constructInstance = null, @@ -53,9 +54,10 @@ public virtual Task SetUp() var methodInfo = testDetails.ClassType .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .First(_ => _.Name == testDetails.MethodName && !_.IsGenericMethod); - var mode = DbAttributeReader.Read(methodInfo, testDetails.ClassType); + var mode = DbAttributeReader.Read(methodInfo, testDetails.ClassType); isSharedDb = mode == DbMode.Shared; isPooledDb = mode == DbMode.Pooled; + isNoDb = mode == DbMode.None; // AsyncLocal values must be set in the Before hook and propagated via AddAsyncLocalValues CombinationCallback.SetInstance(this); @@ -65,11 +67,17 @@ public virtual Task SetUp() instance.Value = this; context.AddAsyncLocalValues(); + if (isNoDb) + { + return Task.CompletedTask; + } + return Reset(); } public async Task Reset() { + ThrowIfNoDb(); phase = Phase.Arrange; var context = TestContext.Current!; var testDetails = context.Metadata.TestDetails; @@ -148,10 +156,19 @@ static Storage GetStorage(Assembly callingAssembly) => public SqlDatabase Database { get; private set; } = null!; + void ThrowIfNoDb() + { + if (isNoDb) + { + throw new("The test is marked [NoDb], so it has no database. Remove [NoDb] to use ArrangeData, ActData, AssertData, or Reset."); + } + } + public virtual T ArrangeData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { throw new("Phase has already moved to Act. Check for a ActData usage in the preceding code."); @@ -170,6 +187,7 @@ public virtual T ActData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { return actData; @@ -191,6 +209,7 @@ public virtual T AssertData { get { + ThrowIfNoDb(); if (phase == Phase.Assert) { return Database.NoTrackingContext; diff --git a/src/EfLocalDb.Tests/DbAttributeReaderTests.cs b/src/EfLocalDb.Tests/DbAttributeReaderTests.cs index 3242899d..795188a6 100644 --- a/src/EfLocalDb.Tests/DbAttributeReaderTests.cs +++ b/src/EfLocalDb.Tests/DbAttributeReaderTests.cs @@ -12,6 +12,9 @@ public sealed class PooledAttribute : Attribute; [AttributeUsage(AttributeTargets.All)] public sealed class NewAttribute : Attribute; + [AttributeUsage(AttributeTargets.All)] + public sealed class NoAttribute : Attribute; + public class Plain { public static void None() @@ -37,6 +40,29 @@ public static void SharedAndPooled() public static void PooledAndNew() { } + + [No] + public static void No() + { + } + + [No, New] + public static void NoAndNew() + { + } + } + + [No] + public class NoClass + { + public static void None() + { + } + + [Pooled] + public static void Pooled() + { + } } [Shared] @@ -92,7 +118,7 @@ static Assembly BuildAssembly(params Type[] attributes) => attributes.Select(_ => new CustomAttributeBuilder(_.GetConstructor(Type.EmptyTypes)!, []))); static DbMode Read(string method, Assembly assembly) => - DbAttributeReader.Read( + DbAttributeReader.Read( typeof(T).GetMethod(method, BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)!, typeof(T), assembly); @@ -144,31 +170,50 @@ public void NewClassOptsOutOfAssembly() => public void MethodOverridesNewClass() => AreEqual(DbMode.Pooled, Read("Pooled", noneAssembly)); + [Test] + public void NoMethod() => + AreEqual(DbMode.None, Read("No", pooledAssembly)); + + [Test] + public void NoClassLevel() => + AreEqual(DbMode.None, Read("None", pooledAssembly)); + + [Test] + public void MethodOverridesNoClass() => + AreEqual(DbMode.Pooled, Read("Pooled", noneAssembly)); + + [Test] + public void NoAndNewOnMethodThrows() + { + var exception = Throws(() => Read("NoAndNew", noneAssembly))!; + AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method.", exception.Message); + } + [Test] public void SharedAndPooledOnMethodThrows() { var exception = Throws(() => Read("SharedAndPooled", noneAssembly))!; - AreEqual("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test method.", exception.Message); + AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method.", exception.Message); } [Test] public void PooledAndNewOnMethodThrows() { var exception = Throws(() => Read("PooledAndNew", noneAssembly))!; - AreEqual("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test method.", exception.Message); + AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method.", exception.Message); } [Test] public void BothOnClassThrows() { var exception = Throws(() => Read("Pooled", noneAssembly))!; - AreEqual("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test class.", exception.Message); + AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test class.", exception.Message); } [Test] public void BothOnAssemblyThrows() { var exception = Throws(() => Read("Pooled", bothAssembly))!; - AreEqual("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on an assembly.", exception.Message); + AreEqual("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on an assembly.", exception.Message); } } diff --git a/src/EfLocalDb.Xunit.V3.Tests/NoDbTests.cs b/src/EfLocalDb.Xunit.V3.Tests/NoDbTests.cs new file mode 100644 index 00000000..ac415fd5 --- /dev/null +++ b/src/EfLocalDb.Xunit.V3.Tests/NoDbTests.cs @@ -0,0 +1,37 @@ +public class NoDbTests : LocalDbTestBase +{ + [Fact] + [NoDb] + public void HasNoDatabase() => + Assert.Null(Database); + + [Fact] + [NoDb] + public void ArrangeDataThrows() + { + var exception = Assert.Throws(() => _ = ArrangeData); + Assert.Contains("[NoDb]", exception.Message); + } + + [Fact] + [NoDb] + public Task ResetThrows() => + Assert.ThrowsAsync(Reset); + + [Fact] + public void UnmarkedMethodHasDatabase() => + Assert.NotNull(Database); +} + +[NoDb] +public class ClassLevelNoDbTests : LocalDbTestBase +{ + [Fact] + public void HasNoDatabase() => + Assert.Null(Database); + + [Fact] + [PooledDb] + public void MethodOverridesClass() => + Assert.NotNull(Database.Transaction); +} diff --git a/src/EfLocalDb.Xunit.V3.Tests/PooledAndSharedDbTests.cs b/src/EfLocalDb.Xunit.V3.Tests/PooledAndSharedDbTests.cs index 805926c2..2b24a6d5 100644 --- a/src/EfLocalDb.Xunit.V3.Tests/PooledAndSharedDbTests.cs +++ b/src/EfLocalDb.Xunit.V3.Tests/PooledAndSharedDbTests.cs @@ -10,6 +10,6 @@ public class PooledAndSharedDbTests : LocalDbTestBase public async Task Throws() { var exception = await Assert.ThrowsAsync(() => base.InitializeAsync().AsTask()); - Assert.Equal("[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on a test method.", exception.Message); + Assert.Equal("[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on a test method.", exception.Message); } } diff --git a/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj b/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj index cff96422..206ffa20 100644 --- a/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj +++ b/src/EfLocalDb.Xunit.V3/EfLocalDb.Xunit.V3.csproj @@ -12,6 +12,7 @@ + diff --git a/src/EfLocalDb.Xunit.V3/LocalDbTestBase.cs b/src/EfLocalDb.Xunit.V3/LocalDbTestBase.cs index e37dec4d..d87535de 100644 --- a/src/EfLocalDb.Xunit.V3/LocalDbTestBase.cs +++ b/src/EfLocalDb.Xunit.V3/LocalDbTestBase.cs @@ -12,6 +12,7 @@ public abstract partial class LocalDbTestBase : bool isSharedDb; bool isPooledDb; + bool isNoDb; public static void Initialize( ConstructInstance? constructInstance = null, @@ -49,11 +50,17 @@ public virtual async ValueTask InitializeAsync() } var methodInfo = GetCurrentMethodInfo(); - var mode = DbAttributeReader.Read(methodInfo, GetType()); + var mode = DbAttributeReader.Read(methodInfo, GetType()); isSharedDb = mode == DbMode.Shared; isPooledDb = mode == DbMode.Pooled; + isNoDb = mode == DbMode.None; QueryFilter.Enable(); + if (isNoDb) + { + return; + } + await Reset(); } @@ -67,6 +74,7 @@ MethodInfo GetCurrentMethodInfo() public async Task Reset() { + ThrowIfNoDb(); phase = Phase.Arrange; var type = GetType().FullName!; var member = GetMemberName(); @@ -143,10 +151,19 @@ static Storage GetStorage(Assembly callingAssembly) => public SqlDatabase Database { get; private set; } = null!; + void ThrowIfNoDb() + { + if (isNoDb) + { + throw new("The test is marked [NoDb], so it has no database. Remove [NoDb] to use ArrangeData, ActData, AssertData, or Reset."); + } + } + public virtual T ArrangeData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { throw new("Phase has already moved to Act. Check for a ActData usage in the preceding code."); @@ -165,6 +182,7 @@ public virtual T ActData { get { + ThrowIfNoDb(); if (phase == Phase.Act) { return actData; @@ -186,6 +204,7 @@ public virtual T AssertData { get { + ThrowIfNoDb(); if (phase == Phase.Assert) { return Database.NoTrackingContext; diff --git a/src/EfLocalDb/DbAttributeReader.cs b/src/EfLocalDb/DbAttributeReader.cs index a74a946a..355e6fd6 100644 --- a/src/EfLocalDb/DbAttributeReader.cs +++ b/src/EfLocalDb/DbAttributeReader.cs @@ -1,62 +1,65 @@ -// Decides whether a LocalDbTestBase test runs against a new, shared, or pooled database. -// [NewDb], [SharedDb] and [PooledDb] can be applied to the test method or the test class, and -// [SharedDb] and [PooledDb] also to the assembly. The nearest wins, and a new database is the +// Decides whether a LocalDbTestBase test runs against a new, shared, or pooled database, or none. +// [NewDb], [SharedDb], [PooledDb] and [NoDb] can be applied to the test method or the test class, +// and [SharedDb] and [PooledDb] also to the assembly. The nearest wins, and a new database is the // default. Every level is validated, so two attributes on one level throw even when a nearer level // decides. The attribute types are type parameters because each test framework package compiles // its own copy of them. static class DbAttributeReader { - public static DbMode Read(MethodInfo method, Type type) + public static DbMode Read(MethodInfo method, Type type) where TShared : Attribute where TPooled : Attribute - where TNew : Attribute => - Read(method, type, type.Assembly); + where TNew : Attribute + where TNo : Attribute => + Read(method, type, type.Assembly); - public static DbMode Read(MethodInfo method, Type type, Assembly assembly) + public static DbMode Read(MethodInfo method, Type type, Assembly assembly) where TShared : Attribute where TPooled : Attribute where TNew : Attribute + where TNo : Attribute { - var methodMode = ReadLevel( - method.GetCustomAttribute() != null, - method.GetCustomAttribute() != null, - method.GetCustomAttribute() != null, - "a test method"); - var classMode = ReadLevel( - type.GetCustomAttribute() != null, - type.GetCustomAttribute() != null, - type.GetCustomAttribute() != null, - "a test class"); - var assemblyMode = ReadLevel( - assembly.GetCustomAttribute() != null, - assembly.GetCustomAttribute() != null, - assembly.GetCustomAttribute() != null, - "an assembly"); + var methodMode = ReadLevel(method, "a test method"); + var classMode = ReadLevel(type, "a test class"); + var assemblyMode = ReadLevel(assembly, "an assembly"); return methodMode ?? classMode ?? assemblyMode ?? DbMode.New; } - static DbMode? ReadLevel(bool isShared, bool isPooled, bool isNew, string target) + static DbMode? ReadLevel(ICustomAttributeProvider provider, string target) + where TShared : Attribute + where TPooled : Attribute + where TNew : Attribute + where TNo : Attribute { - if ((isShared && isPooled) || - (isShared && isNew) || - (isPooled && isNew)) + var modes = new List(); + if (provider.IsDefined(typeof(TShared), true)) + { + modes.Add(DbMode.Shared); + } + + if (provider.IsDefined(typeof(TPooled), true)) + { + modes.Add(DbMode.Pooled); + } + + if (provider.IsDefined(typeof(TNew), true)) { - throw new($"[PooledDb], [SharedDb] and [NewDb] are mutually exclusive. Use only one on {target}."); + modes.Add(DbMode.New); } - if (isShared) + if (provider.IsDefined(typeof(TNo), true)) { - return DbMode.Shared; + modes.Add(DbMode.None); } - if (isPooled) + if (modes.Count > 1) { - return DbMode.Pooled; + throw new($"[PooledDb], [SharedDb], [NewDb] and [NoDb] are mutually exclusive. Use only one on {target}."); } - if (isNew) + if (modes.Count == 1) { - return DbMode.New; + return modes[0]; } return null; diff --git a/src/EfLocalDb/DbMode.cs b/src/EfLocalDb/DbMode.cs index 0b9198e6..b60967a1 100644 --- a/src/EfLocalDb/DbMode.cs +++ b/src/EfLocalDb/DbMode.cs @@ -2,5 +2,6 @@ enum DbMode { New, Shared, - Pooled + Pooled, + None } From 10d3f4433c9ed223a057a69343dcc8f40381aace Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 29 Sep 2026 04:20:40 +0000 Subject: [PATCH 2/2] Docs changes --- pages/temporal-helper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/temporal-helper.md b/pages/temporal-helper.md index d6495ce8..f7db8ccd 100644 --- a/pages/temporal-helper.md +++ b/pages/temporal-helper.md @@ -157,7 +157,7 @@ await database.SetHistoryColumn( // Materialising that history row now fails on a SqlNullValueException, // because the model reads StatusRank into a non-nullable int. That is // the production failure, reproduced in a test. -var exception = CatchAsync( +var exception = await CatchAsync( () => database.Context.Set() .TemporalAll() .Where(_ => _.Id == request.Id)