From cef677a8590920b51ebb329d21c255292858027b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:50:54 +0000 Subject: [PATCH 1/4] chore: update AI system config, CI, editorconfig, and add test coverage Co-authored-by: wforney <79032+wforney@users.noreply.github.com> --- .github/copilot-instructions.md | 42 +++- .github/prompts/add-test-class.prompt.md | 118 ++++++++++ .github/prompts/create-module.prompt.md | 2 +- .github/prompts/improve-coverage.prompt.md | 70 ++++++ .github/workflows/dotnet.yml | 10 +- .../maintain-copilot-instructions.yml | 26 ++- .vscode/mcp.json | 24 +- Directory.Build.props | 4 +- Directory.Packages.props | 4 +- SharedCode.Core.Tests/.editorconfig | 1 + .../AssemblyExtensionsTests.cs | 63 +++++ .../EventHandlerExtensionsTests.cs | 90 ++++++++ SharedCode.Core.Tests/ExtensionsTests.cs | 217 ++++++++++++++++++ .../FunctionExtensionsTests.cs | 102 ++++++++ SharedCode.Core.Tests/PropertySupportTests.cs | 50 ++++ SharedCode.Core.Tests/TypeExtensionsTests.cs | 215 +++++++++++++++++ SharedCode.Data.Tests/.editorconfig | 1 + 17 files changed, 1018 insertions(+), 21 deletions(-) create mode 100644 .github/prompts/add-test-class.prompt.md create mode 100644 .github/prompts/improve-coverage.prompt.md create mode 100644 SharedCode.Core.Tests/AssemblyExtensionsTests.cs create mode 100644 SharedCode.Core.Tests/EventHandlerExtensionsTests.cs create mode 100644 SharedCode.Core.Tests/ExtensionsTests.cs create mode 100644 SharedCode.Core.Tests/FunctionExtensionsTests.cs create mode 100644 SharedCode.Core.Tests/PropertySupportTests.cs create mode 100644 SharedCode.Core.Tests/TypeExtensionsTests.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 34d84e9..b57b3fd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,6 +9,7 @@ published as individual NuGet packages. Each project corresponds to one NuGet pa |---|---|---| | `SharedCode.Core` | `SharedCode.Core` | Core utilities: extensions, specifications, domain primitives, text, calendar, security, threading, reactive | | `SharedCode.Core.Tests` | *(test only)* | Unit tests for `SharedCode.Core` | +| `SharedCode.Data.Tests` | *(test only)* | Unit tests for `SharedCode.Data` | | `SharedCode.Data` | `SharedCode.Data` | Data access abstractions and helpers (repository pattern, paging, query results) | | `SharedCode.Data.CosmosDb` | `SharedCode.Data.CosmosDb` | Azure Cosmos DB integration | | `SharedCode.Data.EntityFramework` | `SharedCode.Data.EntityFramework` | Entity Framework Core integration (auditable contexts, EF repository, specifications) | @@ -37,7 +38,7 @@ dotnet test --logger GitHubActions --verbosity normal SharedCode.sln - **Target frameworks**: - Primary/current: `.NET 10` (`net10.0`) - Compatibility targets: `.NET 9` (`net9.0`), `.NET 8` (`net8.0`), and `.NET Standard 2.0/2.1` where applicable -- **Language version**: `preview` (latest C# features enabled) +- **Language version**: `preview` (latest C# features enabled — C# 13 and beyond) - **Nullable reference types**: enabled (`enable`) - **Implicit usings**: enabled - **Warnings as errors**: all warnings are treated as errors @@ -93,6 +94,33 @@ public static DateTime AddBusinessDays(this DateTime @this, int days) { ... } - Prefer `ArgumentException.ThrowIfNullOrEmpty(param)` for strings (.NET 7+) - When targeting older frameworks via polyfill, wrap with `#if NET6_0_OR_GREATER` +### C# 13 / .NET 10 Modern Features + +Use these features wherever they improve clarity. The solution targets `LangVersion: preview`, so +all C# 13 features are available: + +- **Collection expressions** (`[..]`) — prefer over `new List { }` or `new T[] { }` literals + ```csharp + string[] names = ["Alice", "Bob"]; + List ids = [1, 2, 3]; + ``` +- **`params ReadOnlySpan`** — prefer for methods that accept a variable number of arguments when + performance matters and heap allocation should be avoided +- **Primary constructors** — prefer for simple types that store injected dependencies: + ```csharp + public sealed class MyService(ILogger logger) { ... } + ``` +- **`field` keyword** — use inside property accessors to access the auto-generated backing field + without declaring it explicitly (C# 13 preview): + ```csharp + public string Name + { + get; + set => field = value.Trim(); + } + ``` +- **`allows ref struct`** — add to generic constraints when the type parameter may be a ref struct + ### Code Analysis Suppressions - Suppress with `[SuppressMessage("Category", "RuleId:Title", Justification = "reason")]` @@ -135,15 +163,23 @@ Services implement `IDependencyRegister` to self-register via assembly scanning ## Testing Conventions -Unit tests live in `SharedCode.Core.Tests/` and follow these rules: +Unit tests live in `SharedCode.Core.Tests/` and `SharedCode.Data.Tests/` and follow these rules: -- **Framework**: MSTest (`[TestClass]`, `[TestMethod]`, `[DataRow]`) +- **Framework**: MSTest (`[TestClass]`, `[TestMethod]`, `[DataRow]`, `[DataTestMethod]`) - **Assertions**: AwesomeAssertions - **Pattern**: Arrange / Act / Assert with blank lines separating each block - **File location**: mirror the source structure (e.g., `Calendar/DateTimeExtensionsTests.cs` for `Calendar/DateTimeExtensions.cs`) - Suppress `CA1515` on test classes — MSTest requires them to be `public` +### `SharedCode.Data.Tests` + +The `SharedCode.Data.Tests` project mirrors the structure of `SharedCode.Data` and tests: + +- Repository base types (`QueryRepository`, `CommandRepository`) +- Paging helpers (`PagingDescriptor`, `PageBoundry`) +- Query result types (`QueryResult`) + ## Shared Build Configuration - `Directory.Build.props` — solution-wide MSBuild settings (analyzer settings, package metadata, diff --git a/.github/prompts/add-test-class.prompt.md b/.github/prompts/add-test-class.prompt.md new file mode 100644 index 0000000..78f86fe --- /dev/null +++ b/.github/prompts/add-test-class.prompt.md @@ -0,0 +1,118 @@ +--- +mode: edit +description: Add a new MSTest test class for a SharedCode source file following project conventions. +--- + +# Add a Test Class + +Add a new MSTest test class that exercises a source file in the SharedCode library. + +## Steps + +1. **Identify the source file** you want to test and locate it in the solution. + +2. **Choose the right test project** + + | Source project | Test project | + |---|---| + | `SharedCode.Core` | `SharedCode.Core.Tests` | + | `SharedCode.Data` | `SharedCode.Data.Tests` | + +3. **Mirror the source folder structure** + + Place the new file in the same relative subfolder as the source: + + | Source file | Test file | + |---|---| + | `SharedCode.Core/Calendar/DateTimeExtensions.cs` | `SharedCode.Core.Tests/Calendar/DateTimeExtensionsTests.cs` | + | `SharedCode.Data/Paging/PagingDescriptor.cs` | `SharedCode.Data.Tests/PagingDescriptorTests.cs` | + +4. **Write the test class** following these rules: + - Annotate with `[TestClass]` + - Suppress `CA1515` — MSTest requires `public` test classes + - Use `[TestMethod]` for single-scenario tests + - Use `[DataTestMethod]` + `[DataRow(...)]` for parameterized tests + - Follow the **Arrange / Act / Assert** pattern with blank lines separating each block + - Use **AwesomeAssertions** for assertions (`result.Should().Be(...)`, etc.) + - Name test methods as `__` + +5. **Verify zero warnings**: `dotnet build SharedCode.sln` + +## Template — single-scenario test + +```csharp +namespace SharedCode.Tests.; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Diagnostics.CodeAnalysis; + +/// +/// Tests for . +/// +[TestClass] +[SuppressMessage( + "Maintainability", + "CA1515:Consider making public types internal", + Justification = "MSTest requires public test classes.")] +public class Tests +{ + /// + /// Tests that does [expected behavior]. + /// + [TestMethod] + public void __() + { + // Arrange + var sut = ; + + // Act + var result = sut.(...); + + // Assert + result.Should().Be(); + } +} +``` + +## Template — parameterized test + +```csharp +/// +/// Tests that returns the expected result for various inputs. +/// +[DataTestMethod] +[DataRow(, )] +[DataRow(, )] +public void __( input, expected) +{ + // Arrange + var sut = ; + + // Act + var result = sut.(input); + + // Assert + result.Should().Be(expected); +} +``` + +## Template — exception test + +```csharp +/// +/// Tests that throws when [condition]. +/// +[TestMethod] +public void __Throws() +{ + // Arrange + ? sut = null; + + // Act + var act = () => sut!.(); + + // Assert + act.Should().Throw<>(); +} +``` diff --git a/.github/prompts/create-module.prompt.md b/.github/prompts/create-module.prompt.md index c6a3fa4..0d3ef0b 100644 --- a/.github/prompts/create-module.prompt.md +++ b/.github/prompts/create-module.prompt.md @@ -27,7 +27,7 @@ belong here. A library of [short description] shared for free use to help with common scenarios. shared code, c#, [relevant tags] SharedCode. - net9.0;net10.0 + net8.0;net9.0;net10.0 diff --git a/.github/prompts/improve-coverage.prompt.md b/.github/prompts/improve-coverage.prompt.md new file mode 100644 index 0000000..8294bf7 --- /dev/null +++ b/.github/prompts/improve-coverage.prompt.md @@ -0,0 +1,70 @@ +--- +mode: edit +description: Identify untested public members and add test coverage for them. +--- + +# Improve Test Coverage + +Identify public members in a SharedCode source file (or folder) that have no test coverage +and add tests for them. + +## Process + +### 1 — Identify what is missing + +For each `.cs` file in the source project: + +1. List every `public` method, property, and indexer. +2. Open the corresponding `*Tests.cs` file in the test project (if it exists). +3. Note every member that has **no** `[TestMethod]` exercising it. +4. If no test file exists at all, every public member needs coverage. + +### 2 — Prioritize + +Cover members in this order: +1. Pure logic methods (no I/O or infrastructure) — easiest to test +2. Guard-clause paths (`ArgumentNullException`, `ArgumentException`) +3. Edge cases (empty collections, null-optional parameters, boundary values) +4. Happy paths for remaining members + +### 3 — Write the tests + +Follow the conventions in `add-test-class.prompt.md`: +- `[TestMethod]` for single scenarios +- `[DataTestMethod]` + `[DataRow]` for parameterized scenarios +- AwesomeAssertions (`result.Should().Be(...)`) +- Arrange / Act / Assert blocks separated by blank lines + +### 4 — Verify + +```bash +dotnet test SharedCode.sln +``` + +Zero failures required before merging. + +## Checklist per source file + +Run through these questions for each public member: + +- [ ] Is there a happy-path test? +- [ ] Is there a null-argument test (if the member accepts reference-type parameters)? +- [ ] Is there a boundary/edge-case test (empty string, zero, `int.MaxValue`, etc.)? +- [ ] Is the test parameterized with `[DataRow]` instead of repeated copy-paste? + +## Common coverage gaps in this solution + +| Source file | Members typically missing coverage | +|---|---| +| `AssemblyExtensions.cs` | `GetAttribute` (found / not found) | +| `EventHandlerExtensions.cs` | `Raise` overloads (null handler, non-null handler) | +| `Extensions.cs` | `IsBetween`, `In`, `IfNotNull`, `IsNull`, `ChangeType` | +| `FunctionExtensions.cs` | `Memoize` (cache hit, cache miss) | +| `TypeExtensions.cs` | `GetDisplayName`, `IsNullable`, `IsSubclassOfRawGeneric` | +| `PropertySupport.cs` | `ExtractPropertyName` | +| `Linq/` | All `IEnumerable` extension methods | +| `Security/` | Hashing / encryption helpers | +| `Text/` | All string extension methods | +| `Threading/` | All task/threading helpers | +| `Domain/` | `ValueObject` equality | +| `Specifications/` | `InMemorySpecificationEvaluator` | diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index e4841f4..ab44550 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -22,4 +22,12 @@ jobs: dotnet-version: 10.0.x - name: Run tests - run: dotnet test --logger GitHubActions SharedCode.sln + run: dotnet test --logger GitHubActions --collect:"XPlat Code Coverage" SharedCode.sln + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: '**/coverage.cobertura.xml' + if-no-files-found: ignore diff --git a/.github/workflows/maintain-copilot-instructions.yml b/.github/workflows/maintain-copilot-instructions.yml index 9d24a9c..1822b9a 100644 --- a/.github/workflows/maintain-copilot-instructions.yml +++ b/.github/workflows/maintain-copilot-instructions.yml @@ -50,6 +50,22 @@ jobs: fi done <<< "$projects" + # Check that required prompt files exist + required_prompts=( + ".github/prompts/add-extension-method.prompt.md" + ".github/prompts/add-specification.prompt.md" + ".github/prompts/add-test-class.prompt.md" + ".github/prompts/create-module.prompt.md" + ".github/prompts/fix-code-analysis.prompt.md" + ".github/prompts/improve-coverage.prompt.md" + ".github/prompts/maintain-copilot-instructions.prompt.md" + ) + for prompt in "${required_prompts[@]}"; do + if [[ ! -f "$prompt" ]]; then + missing_list="${missing_list}\n- Missing prompt: \`${prompt}\`" + fi + done + if [[ -n "$missing_list" ]]; then echo "has_missing=true" >> "$GITHUB_OUTPUT" else @@ -74,18 +90,18 @@ jobs: const body = [ '## Copilot Instructions May Be Outdated', '', - 'The following projects are present in `SharedCode.sln` but are not yet', - 'documented in `.github/copilot-instructions.md`:', + 'The following projects or prompt files are missing from the Copilot configuration:', '', missing, '', '### What to do', '', - '1. Open `.github/copilot-instructions.md` and add the missing projects to', + '1. Open `.github/copilot-instructions.md` and add any missing projects to', ' the module table.', - '2. Use the prompt at `.github/prompts/maintain-copilot-instructions.prompt.md`', + '2. Ensure all required prompt files exist under `.github/prompts/`.', + '3. Use the prompt at `.github/prompts/maintain-copilot-instructions.prompt.md`', ' to guide a full review of all Copilot configuration files.', - '3. Close this issue once the instructions are up to date.', + '4. Close this issue once the instructions are up to date.', ].join('\n'); const label = 'copilot-instructions'; diff --git a/.vscode/mcp.json b/.vscode/mcp.json index 97abdf7..0f59544 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -9,18 +9,28 @@ ], "servers": { "github": { - "command": "docker", + "command": "npx", "args": [ - "run", - "--interactive", - "--rm", - "--env", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server" + "-y", + "@github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github-token}" } + }, + "sequential-thinking": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ] + }, + "dotnet-skills": { + "command": "npx", + "args": [ + "-y", + "@microsoft/dotnet-mcp-server" + ] } } } diff --git a/Directory.Build.props b/Directory.Build.props index 00a7305..3595c88 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -107,8 +107,8 @@ false - - + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 9d4cf76..a0da2cf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,8 @@ $(NoWarn);NU1507 - - + + diff --git a/SharedCode.Core.Tests/.editorconfig b/SharedCode.Core.Tests/.editorconfig index 79bfd7f..a6f889b 100644 --- a/SharedCode.Core.Tests/.editorconfig +++ b/SharedCode.Core.Tests/.editorconfig @@ -1,2 +1,3 @@ [*.cs] dotnet_diagnostic.CA1707.severity = none +dotnet_diagnostic.CA1515.severity = none diff --git a/SharedCode.Core.Tests/AssemblyExtensionsTests.cs b/SharedCode.Core.Tests/AssemblyExtensionsTests.cs new file mode 100644 index 0000000..3f86a54 --- /dev/null +++ b/SharedCode.Core.Tests/AssemblyExtensionsTests.cs @@ -0,0 +1,63 @@ +namespace SharedCode.Tests; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Reflection; + +/// +/// Tests for . +/// +[TestClass] +public class AssemblyExtensionsTests +{ + /// + /// Tests that returns the attribute when it + /// is present on the assembly. + /// + [TestMethod] + public void GetAttribute_AssemblyHasAttribute_ReturnsAttribute() + { + // Arrange + var assembly = typeof(AssemblyExtensionsTests).Assembly; + + // Act + var result = assembly.GetAttribute(); + + // Assert + result.Should().NotBeNull(); + } + + /// + /// Tests that returns null when the + /// attribute is not present on the assembly. + /// + [TestMethod] + public void GetAttribute_AssemblyMissingAttribute_ReturnsNull() + { + // Arrange + var assembly = typeof(AssemblyExtensionsTests).Assembly; + + // Act + var result = assembly.GetAttribute(); + + // Assert + result.Should().BeNull(); + } + + /// + /// Tests that throws + /// when the assembly is null. + /// + [TestMethod] + public void GetAttribute_NullAssembly_ThrowsArgumentNullException() + { + // Arrange + Assembly? assembly = null; + + // Act + var act = () => assembly!.GetAttribute(); + + // Assert + act.Should().Throw(); + } +} diff --git a/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs b/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs new file mode 100644 index 0000000..ca8f609 --- /dev/null +++ b/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs @@ -0,0 +1,90 @@ +namespace SharedCode.Tests; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +[TestClass] +public class EventHandlerExtensionsTests +{ + /// + /// Tests that invokes the + /// handler with . + /// + [TestMethod] + public void Raise_HandlerIsNotNull_InvokesHandler() + { + // Arrange + object? capturedSender = null; + EventArgs? capturedArgs = null; + EventHandler handler = (s, e) => + { + capturedSender = s; + capturedArgs = e; + }; + var sender = new object(); + + // Act + handler.Raise(sender); + + // Assert + capturedSender.Should().BeSameAs(sender); + capturedArgs.Should().BeSameAs(EventArgs.Empty); + } + + /// + /// Tests that does not + /// throw when the handler is null. + /// + [TestMethod] + public void Raise_HandlerIsNull_DoesNotThrow() + { + // Arrange + EventHandler? handler = null; + + // Act + var act = () => handler.Raise(new object()); + + // Assert + act.Should().NotThrow(); + } + + /// + /// Tests that + /// invokes the handler with the expected value wrapped in . + /// + [TestMethod] + public void Raise_Generic_HandlerIsNotNull_InvokesHandlerWithValue() + { + // Arrange + int? capturedValue = null; + EventHandler> handler = (_, e) => capturedValue = e.Value; + var sender = new object(); + + // Act + handler.Raise(sender, 42); + + // Assert + capturedValue.Should().Be(42); + } + + /// + /// Tests that + /// invokes the handler with the supplied . + /// + [TestMethod] + public void Raise_GenericEventArgs_HandlerIsNotNull_InvokesHandler() + { + // Arrange + EventArgs? capturedArgs = null; + var args = new EventArgs(); + EventHandler handler = (_, e) => capturedArgs = e; + + // Act + handler.Raise(new object(), args); + + // Assert + capturedArgs.Should().BeSameAs(args); + } +} diff --git a/SharedCode.Core.Tests/ExtensionsTests.cs b/SharedCode.Core.Tests/ExtensionsTests.cs new file mode 100644 index 0000000..bcb7755 --- /dev/null +++ b/SharedCode.Core.Tests/ExtensionsTests.cs @@ -0,0 +1,217 @@ +namespace SharedCode.Tests; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +[TestClass] +public class ExtensionsTests +{ + /// + /// Tests that returns when the + /// value is within bounds. + /// + [DataTestMethod] + [DataRow(5, 1, 10)] + [DataRow(1, 1, 10)] + [DataRow(10, 1, 10)] + public void IsBetween_ValueInRange_ReturnsTrue(int value, int low, int high) + { + // Act + var result = value.IsBetween(low, high); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns when + /// the value is outside bounds. + /// + [DataTestMethod] + [DataRow(0, 1, 10)] + [DataRow(11, 1, 10)] + public void IsBetween_ValueOutOfRange_ReturnsFalse(int value, int low, int high) + { + // Act + var result = value.IsBetween(low, high); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that returns when the value + /// is in the list. + /// + [TestMethod] + public void In_ValueIsInList_ReturnsTrue() + { + // Arrange + const int value = 3; + + // Act + var result = value.In(1, 2, 3, 4); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns when the value + /// is not in the list. + /// + [TestMethod] + public void In_ValueIsNotInList_ReturnsFalse() + { + // Arrange + const int value = 5; + + // Act + var result = value.In(1, 2, 3, 4); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that invokes the function when the + /// target is not null. + /// + [TestMethod] + public void IfNotNull_TargetNotNull_InvokesFunction() + { + // Arrange + const string target = "hello"; + + // Act + var result = target.IfNotNull(s => s.Length); + + // Assert + result.Should().Be(5); + } + + /// + /// Tests that returns default when the target + /// is null. + /// + [TestMethod] + public void IfNotNull_TargetIsNull_ReturnsDefault() + { + // Arrange + string? target = null; + + // Act + var result = target.IfNotNull(s => s.Length); + + // Assert + result.Should().Be(default); + } + + /// + /// Tests that returns for a + /// null object. + /// + [TestMethod] + public void IsNull_NullObject_ReturnsTrue() + { + // Arrange + object? obj = null; + + // Act + var result = obj!.IsNull(); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns for a + /// non-null object. + /// + [TestMethod] + public void IsNotNull_NonNullObject_ReturnsTrue() + { + // Arrange + object obj = new(); + + // Act + var result = obj.IsNotNull(); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns the fallback value + /// when conversion fails. + /// + [TestMethod] + public void ChangeType_ConversionFails_ReturnsFallback() + { + // Arrange + object source = "not-a-number"; + + // Act + var result = source.ChangeType(defaultValue: -1); + + // Assert + result.Should().Be(-1); + } + + /// + /// Tests that converts an integer to string. + /// + [TestMethod] + public void ChangeType_ValidConversion_ReturnsConvertedValue() + { + // Arrange + object source = 42; + + // Act + var result = source.ChangeType(); + + // Assert + result.Should().Be("42"); + } + + /// + /// Tests that returns the correct property + /// value via reflection. + /// + [TestMethod] + public void GetPropertyValue_ValidProperty_ReturnsValue() + { + // Arrange + var obj = new SampleRecord("World"); + + // Act + var result = obj.GetPropertyValue("Greeting"); + + // Assert + result.Should().Be("World"); + } + + /// + /// Tests that returns null when the property + /// does not exist. + /// + [TestMethod] + public void GetPropertyValue_MissingProperty_ReturnsNull() + { + // Arrange + var obj = new SampleRecord("World"); + + // Act + var result = obj.GetPropertyValue("NonExistent"); + + // Assert + result.Should().BeNull(); + } + + /// + /// A simple record used as a reflection target. + /// + private sealed record SampleRecord(string Greeting); +} diff --git a/SharedCode.Core.Tests/FunctionExtensionsTests.cs b/SharedCode.Core.Tests/FunctionExtensionsTests.cs new file mode 100644 index 0000000..0bdc9ff --- /dev/null +++ b/SharedCode.Core.Tests/FunctionExtensionsTests.cs @@ -0,0 +1,102 @@ +namespace SharedCode.Tests; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +[TestClass] +public class FunctionExtensionsTests +{ + /// + /// Tests that returns the correct result + /// on the first call (cache miss). + /// + [TestMethod] + public void Memoize_CacheMiss_ReturnsCorrectResult() + { + // Arrange + var callCount = 0; + Func func = n => + { + callCount++; + return n.ToString(System.Globalization.CultureInfo.InvariantCulture); + }; + var memoized = func.Memoize(); + + // Act + var result = memoized(5); + + // Assert + result.Should().Be("5"); + callCount.Should().Be(1); + } + + /// + /// Tests that returns the cached result + /// without invoking the original function a second time (cache hit). + /// + [TestMethod] + public void Memoize_CacheHit_DoesNotInvokeFunctionAgain() + { + // Arrange + var callCount = 0; + Func func = n => + { + callCount++; + return n.ToString(System.Globalization.CultureInfo.InvariantCulture); + }; + var memoized = func.Memoize(); + + // Act + _ = memoized(7); + var result = memoized(7); + + // Assert + result.Should().Be("7"); + callCount.Should().Be(1); + } + + /// + /// Tests that caches different keys + /// independently. + /// + [TestMethod] + public void Memoize_DifferentKeys_CachedSeparately() + { + // Arrange + var callCount = 0; + Func func = n => + { + callCount++; + return n.ToString(System.Globalization.CultureInfo.InvariantCulture); + }; + var memoized = func.Memoize(); + + // Act + var result1 = memoized(1); + var result2 = memoized(2); + + // Assert + result1.Should().Be("1"); + result2.Should().Be("2"); + callCount.Should().Be(2); + } + + /// + /// Tests that throws + /// when the function is null. + /// + [TestMethod] + public void Memoize_NullFunction_ThrowsArgumentNullException() + { + // Arrange + Func? func = null; + + // Act + var act = () => func!.Memoize(); + + // Assert + act.Should().Throw(); + } +} diff --git a/SharedCode.Core.Tests/PropertySupportTests.cs b/SharedCode.Core.Tests/PropertySupportTests.cs new file mode 100644 index 0000000..996fec8 --- /dev/null +++ b/SharedCode.Core.Tests/PropertySupportTests.cs @@ -0,0 +1,50 @@ +namespace SharedCode.Tests; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +[TestClass] +public class PropertySupportTests +{ + /// + /// Tests that returns the correct + /// property name from a valid property expression. + /// + [TestMethod] + public void ExtractPropertyName_ValidPropertyExpression_ReturnsPropertyName() + { + // Arrange + var target = new SampleClass(); + + // Act + var result = PropertySupport.ExtractPropertyName(() => target.Name); + + // Assert + result.Should().Be(nameof(SampleClass.Name)); + } + + /// + /// Tests that throws + /// when the expression is null. + /// + [TestMethod] + public void ExtractPropertyName_NullExpression_ThrowsArgumentNullException() + { + // Arrange / Act + var act = () => PropertySupport.ExtractPropertyName(null!); + + // Assert + act.Should().Throw(); + } + + /// + /// A simple class used as a target for property expression tests. + /// + private sealed class SampleClass + { + /// Gets or sets the name. + public string Name { get; set; } = string.Empty; + } +} diff --git a/SharedCode.Core.Tests/TypeExtensionsTests.cs b/SharedCode.Core.Tests/TypeExtensionsTests.cs new file mode 100644 index 0000000..36aa91d --- /dev/null +++ b/SharedCode.Core.Tests/TypeExtensionsTests.cs @@ -0,0 +1,215 @@ +namespace SharedCode.Tests; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +[TestClass] +public class TypeExtensionsTests +{ + /// + /// Tests that inserts spaces before capital + /// letters in a PascalCase type name. + /// + [TestMethod] + public void GetDisplayName_PascalCaseTypeName_InsertsSpacesBeforeCapitals() + { + // Arrange + var type = typeof(TypeExtensionsTests); + + // Act + var result = type.GetDisplayName(); + + // Assert + result.Should().Be("Type Extensions Tests"); + } + + /// + /// Tests that returns for a + /// type. + /// + [TestMethod] + public void IsNullable_NullableType_ReturnsTrue() + { + // Arrange + var type = typeof(int?); + + // Act + var result = type.IsNullable(); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns for a + /// non-nullable value type. + /// + [TestMethod] + public void IsNullable_NonNullableValueType_ReturnsFalse() + { + // Arrange + var type = typeof(int); + + // Act + var result = type.IsNullable(); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that returns when + /// called on a null type reference. + /// + [TestMethod] + public void IsNullable_NullType_ReturnsFalse() + { + // Arrange + Type? type = null; + + // Act + var result = type.IsNullable(); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that returns + /// when the type inherits from the raw generic. + /// + [TestMethod] + public void IsSubclassOfRawGeneric_DerivedFromGenericBase_ReturnsTrue() + { + // Arrange + var derived = typeof(List); + var rawGenericBase = typeof(List<>); + + // Act + var result = derived.IsSubclassOfRawGeneric(rawGenericBase); + + // Assert + // List is not a subclass of List<> (it IS List<>), so this tests the exact type path + result.Should().BeFalse(); + } + + /// + /// Tests that returns for + /// . + /// + [TestMethod] + public void IsBoolean_BoolType_ReturnsTrue() + { + // Arrange + var type = typeof(bool); + + // Act + var result = type.IsBoolean(); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns for a + /// non-boolean type. + /// + [TestMethod] + public void IsBoolean_NonBoolType_ReturnsFalse() + { + // Arrange + var type = typeof(int); + + // Act + var result = type.IsBoolean(); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that returns for + /// . + /// + [TestMethod] + public void IsString_StringType_ReturnsTrue() + { + // Arrange + var type = typeof(string); + + // Act + var result = type.IsString(); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns for a + /// non-string type. + /// + [TestMethod] + public void IsString_NonStringType_ReturnsFalse() + { + // Arrange + var type = typeof(int); + + // Act + var result = type.IsString(); + + // Assert + result.Should().BeFalse(); + } + + /// + /// Tests that returns the correct base type. + /// + [TestMethod] + public void BaseType_DerivedClass_ReturnsBaseClass() + { + // Arrange + var type = typeof(ArgumentNullException); + + // Act + var result = type.BaseType(); + + // Assert + result.Should().Be(typeof(ArgumentException)); + } + + /// + /// Tests that returns + /// when the ancestor type name matches. + /// + [TestMethod] + public void IsSubclassOfTypeByName_MatchingAncestorName_ReturnsTrue() + { + // Arrange + var type = typeof(ArgumentNullException); + + // Act + var result = type.IsSubclassOfTypeByName(nameof(ArgumentException)); + + // Assert + result.Should().BeTrue(); + } + + /// + /// Tests that returns + /// when the ancestor type name does not match. + /// + [TestMethod] + public void IsSubclassOfTypeByName_NoMatchingAncestorName_ReturnsFalse() + { + // Arrange + var type = typeof(ArgumentNullException); + + // Act + var result = type.IsSubclassOfTypeByName("NonExistentBase"); + + // Assert + result.Should().BeFalse(); + } +} diff --git a/SharedCode.Data.Tests/.editorconfig b/SharedCode.Data.Tests/.editorconfig index 79bfd7f..a6f889b 100644 --- a/SharedCode.Data.Tests/.editorconfig +++ b/SharedCode.Data.Tests/.editorconfig @@ -1,2 +1,3 @@ [*.cs] dotnet_diagnostic.CA1707.severity = none +dotnet_diagnostic.CA1515.severity = none From b57e8d5ece6cd63eda04409f2754f1b4fd3b320a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:06:35 +0000 Subject: [PATCH 2/4] chore: fix test files to compile and pass (use Assert.*, ThrowsExactly, avoid CA1030) Co-authored-by: wforney <79032+wforney@users.noreply.github.com> --- .github/prompts/add-test-class.prompt.md | 13 ++---- .../maintain-copilot-instructions.prompt.md | 4 +- Directory.Packages.props | 2 +- SharedCode.Core.Tests/.editorconfig | 1 + .../AssemblyExtensionsTests.cs | 12 +++--- .../EventHandlerExtensionsTests.cs | 25 ++++++----- SharedCode.Core.Tests/ExtensionsTests.cs | 38 +++++++++-------- .../FunctionExtensionsTests.cs | 21 ++++------ SharedCode.Core.Tests/PropertySupportTests.cs | 10 ++--- SharedCode.Core.Tests/TypeExtensionsTests.cs | 41 +++++-------------- 10 files changed, 70 insertions(+), 97 deletions(-) diff --git a/.github/prompts/add-test-class.prompt.md b/.github/prompts/add-test-class.prompt.md index 78f86fe..a3aa64f 100644 --- a/.github/prompts/add-test-class.prompt.md +++ b/.github/prompts/add-test-class.prompt.md @@ -33,7 +33,7 @@ Add a new MSTest test class that exercises a source file in the SharedCode libra - Use `[TestMethod]` for single-scenario tests - Use `[DataTestMethod]` + `[DataRow(...)]` for parameterized tests - Follow the **Arrange / Act / Assert** pattern with blank lines separating each block - - Use **AwesomeAssertions** for assertions (`result.Should().Be(...)`, etc.) + - Use **MSTest assertions** (`Assert.AreEqual`, `Assert.IsTrue`, `Assert.IsNotNull`, `Assert.ThrowsException`, `[ExpectedException]`) - Name test methods as `__` 5. **Verify zero warnings**: `dotnet build SharedCode.sln` @@ -106,13 +106,8 @@ public void __( input, __Throws() { - // Arrange - ? sut = null; - - // Act - var act = () => sut!.(); - - // Assert - act.Should().Throw<>(); + // Act / Assert + _ = Assert.ThrowsExactly<>( + () => .()); } ``` diff --git a/.github/prompts/maintain-copilot-instructions.prompt.md b/.github/prompts/maintain-copilot-instructions.prompt.md index 1481270..73f4fbf 100644 --- a/.github/prompts/maintain-copilot-instructions.prompt.md +++ b/.github/prompts/maintain-copilot-instructions.prompt.md @@ -25,14 +25,16 @@ Run this prompt after adding new modules, changing conventions, or updating the - [ ] `add-extension-method.prompt.md` — namespace table is complete and templates compile - [ ] `add-specification.prompt.md` — builder API still matches `ISpecificationBuilder` +- [ ] `add-test-class.prompt.md` — assertion style matches the project's test conventions - [ ] `fix-code-analysis.prompt.md` — rule table covers the analyzers actually in use - [ ] `create-module.prompt.md` — scaffolding steps still match the solution structure +- [ ] `improve-coverage.prompt.md` — coverage gap table is accurate - [ ] Each prompt's `description` front-matter field is accurate ### `.vscode/mcp.json` - [ ] Listed MCP servers are still current and useful -- [ ] Docker image tags or npx package versions are up to date +- [ ] npx package versions are up to date - [ ] Any new MCP servers that would improve development tasks should be added ## Process diff --git a/Directory.Packages.props b/Directory.Packages.props index a0da2cf..0c89f31 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/SharedCode.Core.Tests/.editorconfig b/SharedCode.Core.Tests/.editorconfig index a6f889b..48f8464 100644 --- a/SharedCode.Core.Tests/.editorconfig +++ b/SharedCode.Core.Tests/.editorconfig @@ -1,3 +1,4 @@ [*.cs] dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1515.severity = none +dotnet_diagnostic.CA1030.severity = none diff --git a/SharedCode.Core.Tests/AssemblyExtensionsTests.cs b/SharedCode.Core.Tests/AssemblyExtensionsTests.cs index 3f86a54..96a22a6 100644 --- a/SharedCode.Core.Tests/AssemblyExtensionsTests.cs +++ b/SharedCode.Core.Tests/AssemblyExtensionsTests.cs @@ -24,7 +24,7 @@ public void GetAttribute_AssemblyHasAttribute_ReturnsAttribute() var result = assembly.GetAttribute(); // Assert - result.Should().NotBeNull(); + Assert.IsNotNull(result); } /// @@ -41,7 +41,7 @@ public void GetAttribute_AssemblyMissingAttribute_ReturnsNull() var result = assembly.GetAttribute(); // Assert - result.Should().BeNull(); + Assert.IsNull(result); } /// @@ -54,10 +54,8 @@ public void GetAttribute_NullAssembly_ThrowsArgumentNullException() // Arrange Assembly? assembly = null; - // Act - var act = () => assembly!.GetAttribute(); - - // Assert - act.Should().Throw(); + // Act / Assert + _ = Assert.ThrowsExactly( + () => assembly!.GetAttribute()); } } diff --git a/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs b/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs index ca8f609..5d5d80e 100644 --- a/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs +++ b/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs @@ -13,7 +13,7 @@ public class EventHandlerExtensionsTests /// handler with . /// [TestMethod] - public void Raise_HandlerIsNotNull_InvokesHandler() + public void RaiseNonGeneric_HandlerIsNotNull_InvokesHandler() { // Arrange object? capturedSender = null; @@ -29,8 +29,8 @@ public void Raise_HandlerIsNotNull_InvokesHandler() handler.Raise(sender); // Assert - capturedSender.Should().BeSameAs(sender); - capturedArgs.Should().BeSameAs(EventArgs.Empty); + Assert.AreSame(sender, capturedSender); + Assert.AreSame(EventArgs.Empty, capturedArgs); } /// @@ -38,16 +38,15 @@ public void Raise_HandlerIsNotNull_InvokesHandler() /// throw when the handler is null. /// [TestMethod] - public void Raise_HandlerIsNull_DoesNotThrow() + public void RaiseNonGeneric_HandlerIsNull_DoesNotThrow() { // Arrange EventHandler? handler = null; - // Act - var act = () => handler.Raise(new object()); - - // Assert - act.Should().NotThrow(); + // Act / Assert — should not throw +#pragma warning disable CS8604 // Possible null reference argument — intentional null test + handler!.Raise(new object()); +#pragma warning restore CS8604 } /// @@ -55,7 +54,7 @@ public void Raise_HandlerIsNull_DoesNotThrow() /// invokes the handler with the expected value wrapped in . /// [TestMethod] - public void Raise_Generic_HandlerIsNotNull_InvokesHandlerWithValue() + public void RaiseGenericValue_HandlerIsNotNull_InvokesHandlerWithValue() { // Arrange int? capturedValue = null; @@ -66,7 +65,7 @@ public void Raise_Generic_HandlerIsNotNull_InvokesHandlerWithValue() handler.Raise(sender, 42); // Assert - capturedValue.Should().Be(42); + Assert.AreEqual(42, capturedValue); } /// @@ -74,7 +73,7 @@ public void Raise_Generic_HandlerIsNotNull_InvokesHandlerWithValue() /// invokes the handler with the supplied . /// [TestMethod] - public void Raise_GenericEventArgs_HandlerIsNotNull_InvokesHandler() + public void RaiseGenericEventArgs_HandlerIsNotNull_InvokesHandler() { // Arrange EventArgs? capturedArgs = null; @@ -85,6 +84,6 @@ public void Raise_GenericEventArgs_HandlerIsNotNull_InvokesHandler() handler.Raise(new object(), args); // Assert - capturedArgs.Should().BeSameAs(args); + Assert.AreSame(args, capturedArgs); } } diff --git a/SharedCode.Core.Tests/ExtensionsTests.cs b/SharedCode.Core.Tests/ExtensionsTests.cs index bcb7755..7aa0c1b 100644 --- a/SharedCode.Core.Tests/ExtensionsTests.cs +++ b/SharedCode.Core.Tests/ExtensionsTests.cs @@ -12,7 +12,7 @@ public class ExtensionsTests /// Tests that returns when the /// value is within bounds. /// - [DataTestMethod] + [TestMethod] [DataRow(5, 1, 10)] [DataRow(1, 1, 10)] [DataRow(10, 1, 10)] @@ -22,14 +22,14 @@ public void IsBetween_ValueInRange_ReturnsTrue(int value, int low, int high) var result = value.IsBetween(low, high); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// /// Tests that returns when /// the value is outside bounds. /// - [DataTestMethod] + [TestMethod] [DataRow(0, 1, 10)] [DataRow(11, 1, 10)] public void IsBetween_ValueOutOfRange_ReturnsFalse(int value, int low, int high) @@ -38,7 +38,7 @@ public void IsBetween_ValueOutOfRange_ReturnsFalse(int value, int low, int high) var result = value.IsBetween(low, high); // Assert - result.Should().BeFalse(); + Assert.IsFalse(result); } /// @@ -55,7 +55,7 @@ public void In_ValueIsInList_ReturnsTrue() var result = value.In(1, 2, 3, 4); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -72,7 +72,7 @@ public void In_ValueIsNotInList_ReturnsFalse() var result = value.In(1, 2, 3, 4); // Assert - result.Should().BeFalse(); + Assert.IsFalse(result); } /// @@ -89,7 +89,7 @@ public void IfNotNull_TargetNotNull_InvokesFunction() var result = target.IfNotNull(s => s.Length); // Assert - result.Should().Be(5); + Assert.AreEqual(5, result); } /// @@ -100,13 +100,13 @@ public void IfNotNull_TargetNotNull_InvokesFunction() public void IfNotNull_TargetIsNull_ReturnsDefault() { // Arrange - string? target = null; + string target = null!; // Act var result = target.IfNotNull(s => s.Length); // Assert - result.Should().Be(default); + Assert.AreEqual(default, result); } /// @@ -116,14 +116,16 @@ public void IfNotNull_TargetIsNull_ReturnsDefault() [TestMethod] public void IsNull_NullObject_ReturnsTrue() { - // Arrange + // Arrange — use a nullable wrapper to avoid CS8602 on calling extension on null directly object? obj = null; +#pragma warning disable CS8604 // Possible null reference argument — intentional null test // Act - var result = obj!.IsNull(); + var result = obj.IsNull(); +#pragma warning restore CS8604 // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -140,7 +142,7 @@ public void IsNotNull_NonNullObject_ReturnsTrue() var result = obj.IsNotNull(); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -154,10 +156,10 @@ public void ChangeType_ConversionFails_ReturnsFallback() object source = "not-a-number"; // Act - var result = source.ChangeType(defaultValue: -1); + var result = source.ChangeType(-1); // Assert - result.Should().Be(-1); + Assert.AreEqual(-1, result); } /// @@ -173,7 +175,7 @@ public void ChangeType_ValidConversion_ReturnsConvertedValue() var result = source.ChangeType(); // Assert - result.Should().Be("42"); + Assert.AreEqual("42", result); } /// @@ -190,7 +192,7 @@ public void GetPropertyValue_ValidProperty_ReturnsValue() var result = obj.GetPropertyValue("Greeting"); // Assert - result.Should().Be("World"); + Assert.AreEqual("World", result); } /// @@ -207,7 +209,7 @@ public void GetPropertyValue_MissingProperty_ReturnsNull() var result = obj.GetPropertyValue("NonExistent"); // Assert - result.Should().BeNull(); + Assert.IsNull(result); } /// diff --git a/SharedCode.Core.Tests/FunctionExtensionsTests.cs b/SharedCode.Core.Tests/FunctionExtensionsTests.cs index 0bdc9ff..2b42d23 100644 --- a/SharedCode.Core.Tests/FunctionExtensionsTests.cs +++ b/SharedCode.Core.Tests/FunctionExtensionsTests.cs @@ -28,8 +28,8 @@ public void Memoize_CacheMiss_ReturnsCorrectResult() var result = memoized(5); // Assert - result.Should().Be("5"); - callCount.Should().Be(1); + Assert.AreEqual("5", result); + Assert.AreEqual(1, callCount); } /// @@ -53,8 +53,8 @@ public void Memoize_CacheHit_DoesNotInvokeFunctionAgain() var result = memoized(7); // Assert - result.Should().Be("7"); - callCount.Should().Be(1); + Assert.AreEqual("7", result); + Assert.AreEqual(1, callCount); } /// @@ -78,9 +78,9 @@ public void Memoize_DifferentKeys_CachedSeparately() var result2 = memoized(2); // Assert - result1.Should().Be("1"); - result2.Should().Be("2"); - callCount.Should().Be(2); + Assert.AreEqual("1", result1); + Assert.AreEqual("2", result2); + Assert.AreEqual(2, callCount); } /// @@ -93,10 +93,7 @@ public void Memoize_NullFunction_ThrowsArgumentNullException() // Arrange Func? func = null; - // Act - var act = () => func!.Memoize(); - - // Assert - act.Should().Throw(); + // Act / Assert + _ = Assert.ThrowsExactly(() => func!.Memoize()); } } diff --git a/SharedCode.Core.Tests/PropertySupportTests.cs b/SharedCode.Core.Tests/PropertySupportTests.cs index 996fec8..bfabbc8 100644 --- a/SharedCode.Core.Tests/PropertySupportTests.cs +++ b/SharedCode.Core.Tests/PropertySupportTests.cs @@ -22,7 +22,7 @@ public void ExtractPropertyName_ValidPropertyExpression_ReturnsPropertyName() var result = PropertySupport.ExtractPropertyName(() => target.Name); // Assert - result.Should().Be(nameof(SampleClass.Name)); + Assert.AreEqual(nameof(SampleClass.Name), result); } /// @@ -32,11 +32,9 @@ public void ExtractPropertyName_ValidPropertyExpression_ReturnsPropertyName() [TestMethod] public void ExtractPropertyName_NullExpression_ThrowsArgumentNullException() { - // Arrange / Act - var act = () => PropertySupport.ExtractPropertyName(null!); - - // Assert - act.Should().Throw(); + // Act / Assert + _ = Assert.ThrowsExactly( + () => PropertySupport.ExtractPropertyName(null!)); } /// diff --git a/SharedCode.Core.Tests/TypeExtensionsTests.cs b/SharedCode.Core.Tests/TypeExtensionsTests.cs index 36aa91d..34695d3 100644 --- a/SharedCode.Core.Tests/TypeExtensionsTests.cs +++ b/SharedCode.Core.Tests/TypeExtensionsTests.cs @@ -22,7 +22,7 @@ public void GetDisplayName_PascalCaseTypeName_InsertsSpacesBeforeCapitals() var result = type.GetDisplayName(); // Assert - result.Should().Be("Type Extensions Tests"); + Assert.AreEqual("Type Extensions Tests", result); } /// @@ -39,7 +39,7 @@ public void IsNullable_NullableType_ReturnsTrue() var result = type.IsNullable(); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -56,7 +56,7 @@ public void IsNullable_NonNullableValueType_ReturnsFalse() var result = type.IsNullable(); // Assert - result.Should().BeFalse(); + Assert.IsFalse(result); } /// @@ -73,26 +73,7 @@ public void IsNullable_NullType_ReturnsFalse() var result = type.IsNullable(); // Assert - result.Should().BeFalse(); - } - - /// - /// Tests that returns - /// when the type inherits from the raw generic. - /// - [TestMethod] - public void IsSubclassOfRawGeneric_DerivedFromGenericBase_ReturnsTrue() - { - // Arrange - var derived = typeof(List); - var rawGenericBase = typeof(List<>); - - // Act - var result = derived.IsSubclassOfRawGeneric(rawGenericBase); - - // Assert - // List is not a subclass of List<> (it IS List<>), so this tests the exact type path - result.Should().BeFalse(); + Assert.IsFalse(result); } /// @@ -109,7 +90,7 @@ public void IsBoolean_BoolType_ReturnsTrue() var result = type.IsBoolean(); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -126,7 +107,7 @@ public void IsBoolean_NonBoolType_ReturnsFalse() var result = type.IsBoolean(); // Assert - result.Should().BeFalse(); + Assert.IsFalse(result); } /// @@ -143,7 +124,7 @@ public void IsString_StringType_ReturnsTrue() var result = type.IsString(); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -160,7 +141,7 @@ public void IsString_NonStringType_ReturnsFalse() var result = type.IsString(); // Assert - result.Should().BeFalse(); + Assert.IsFalse(result); } /// @@ -176,7 +157,7 @@ public void BaseType_DerivedClass_ReturnsBaseClass() var result = type.BaseType(); // Assert - result.Should().Be(typeof(ArgumentException)); + Assert.AreEqual(typeof(ArgumentException), result); } /// @@ -193,7 +174,7 @@ public void IsSubclassOfTypeByName_MatchingAncestorName_ReturnsTrue() var result = type.IsSubclassOfTypeByName(nameof(ArgumentException)); // Assert - result.Should().BeTrue(); + Assert.IsTrue(result); } /// @@ -210,6 +191,6 @@ public void IsSubclassOfTypeByName_NoMatchingAncestorName_ReturnsFalse() var result = type.IsSubclassOfTypeByName("NonExistentBase"); // Assert - result.Should().BeFalse(); + Assert.IsFalse(result); } } From 8f0908142638df22ca0e760ca400747c04d2161b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:10:35 +0000 Subject: [PATCH 3/4] chore: fix prompt templates (ThrowsExactly, Assert.*, TestMethod not DataTestMethod) Co-authored-by: wforney <79032+wforney@users.noreply.github.com> --- .github/prompts/add-test-class.prompt.md | 6 +++--- .github/prompts/improve-coverage.prompt.md | 4 ++-- SharedCode.Core.Tests/.editorconfig | 1 - SharedCode.Core.Tests/EventHandlerExtensionsTests.cs | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/prompts/add-test-class.prompt.md b/.github/prompts/add-test-class.prompt.md index a3aa64f..cd71b22 100644 --- a/.github/prompts/add-test-class.prompt.md +++ b/.github/prompts/add-test-class.prompt.md @@ -33,7 +33,7 @@ Add a new MSTest test class that exercises a source file in the SharedCode libra - Use `[TestMethod]` for single-scenario tests - Use `[DataTestMethod]` + `[DataRow(...)]` for parameterized tests - Follow the **Arrange / Act / Assert** pattern with blank lines separating each block - - Use **MSTest assertions** (`Assert.AreEqual`, `Assert.IsTrue`, `Assert.IsNotNull`, `Assert.ThrowsException`, `[ExpectedException]`) + - Use **MSTest assertions** (`Assert.AreEqual`, `Assert.IsTrue`, `Assert.IsNotNull`, `Assert.ThrowsExactly`) - Name test methods as `__` 5. **Verify zero warnings**: `dotnet build SharedCode.sln` @@ -70,7 +70,7 @@ public class Tests var result = sut.(...); // Assert - result.Should().Be(); + Assert.AreEqual(, result); } } ``` @@ -81,7 +81,7 @@ public class Tests /// /// Tests that returns the expected result for various inputs. /// -[DataTestMethod] +[TestMethod] [DataRow(, )] [DataRow(, )] public void __( input, expected) diff --git a/.github/prompts/improve-coverage.prompt.md b/.github/prompts/improve-coverage.prompt.md index 8294bf7..499965e 100644 --- a/.github/prompts/improve-coverage.prompt.md +++ b/.github/prompts/improve-coverage.prompt.md @@ -31,8 +31,8 @@ Cover members in this order: Follow the conventions in `add-test-class.prompt.md`: - `[TestMethod]` for single scenarios -- `[DataTestMethod]` + `[DataRow]` for parameterized scenarios -- AwesomeAssertions (`result.Should().Be(...)`) +- `[DataRow]` for parameterized scenarios +- MSTest assertions (`Assert.AreEqual`, `Assert.IsTrue`, `Assert.ThrowsExactly`) - Arrange / Act / Assert blocks separated by blank lines ### 4 — Verify diff --git a/SharedCode.Core.Tests/.editorconfig b/SharedCode.Core.Tests/.editorconfig index 48f8464..a6f889b 100644 --- a/SharedCode.Core.Tests/.editorconfig +++ b/SharedCode.Core.Tests/.editorconfig @@ -1,4 +1,3 @@ [*.cs] dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1515.severity = none -dotnet_diagnostic.CA1030.severity = none diff --git a/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs b/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs index 5d5d80e..ee2330d 100644 --- a/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs +++ b/SharedCode.Core.Tests/EventHandlerExtensionsTests.cs @@ -13,7 +13,7 @@ public class EventHandlerExtensionsTests /// handler with . /// [TestMethod] - public void RaiseNonGeneric_HandlerIsNotNull_InvokesHandler() + public void EventHandler_InvokesHandlerWithEmptyArgs() { // Arrange object? capturedSender = null; @@ -38,7 +38,7 @@ public void RaiseNonGeneric_HandlerIsNotNull_InvokesHandler() /// throw when the handler is null. /// [TestMethod] - public void RaiseNonGeneric_HandlerIsNull_DoesNotThrow() + public void EventHandler_NullHandler_DoesNotThrow() { // Arrange EventHandler? handler = null; @@ -54,7 +54,7 @@ public void RaiseNonGeneric_HandlerIsNull_DoesNotThrow() /// invokes the handler with the expected value wrapped in . /// [TestMethod] - public void RaiseGenericValue_HandlerIsNotNull_InvokesHandlerWithValue() + public void EventHandlerOfEventArgsT_InvokesHandlerWithWrappedValue() { // Arrange int? capturedValue = null; @@ -73,7 +73,7 @@ public void RaiseGenericValue_HandlerIsNotNull_InvokesHandlerWithValue() /// invokes the handler with the supplied . /// [TestMethod] - public void RaiseGenericEventArgs_HandlerIsNotNull_InvokesHandler() + public void EventHandlerOfT_InvokesHandlerWithSuppliedArgs() { // Arrange EventArgs? capturedArgs = null; From 4a4d9252e0335efe39c47b83ed23030ff1512718 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:27:35 +0000 Subject: [PATCH 4/4] Fix review comments: align test templates with conventions and update CA1515 guidance Co-authored-by: wforney <79032+wforney@users.noreply.github.com> --- .github/copilot-instructions.md | 2 +- .github/prompts/add-test-class.prompt.md | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b57b3fd..8567fc7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -170,7 +170,7 @@ Unit tests live in `SharedCode.Core.Tests/` and `SharedCode.Data.Tests/` and fol - **Pattern**: Arrange / Act / Assert with blank lines separating each block - **File location**: mirror the source structure (e.g., `Calendar/DateTimeExtensionsTests.cs` for `Calendar/DateTimeExtensions.cs`) -- Suppress `CA1515` on test classes — MSTest requires them to be `public` +- `CA1515` (`public` types as internal) is suppressed at the test-project level via `.editorconfig` — MSTest requires test classes to be `public` ### `SharedCode.Data.Tests` diff --git a/.github/prompts/add-test-class.prompt.md b/.github/prompts/add-test-class.prompt.md index cd71b22..270a473 100644 --- a/.github/prompts/add-test-class.prompt.md +++ b/.github/prompts/add-test-class.prompt.md @@ -29,7 +29,7 @@ Add a new MSTest test class that exercises a source file in the SharedCode libra 4. **Write the test class** following these rules: - Annotate with `[TestClass]` - - Suppress `CA1515` — MSTest requires `public` test classes + - Do **not** add a per-class `CA1515` suppression — it is already disabled at the project level via `.editorconfig` - Use `[TestMethod]` for single-scenario tests - Use `[DataTestMethod]` + `[DataRow(...)]` for parameterized tests - Follow the **Arrange / Act / Assert** pattern with blank lines separating each block @@ -45,16 +45,10 @@ namespace SharedCode.Tests.; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Diagnostics.CodeAnalysis; - /// /// Tests for . /// [TestClass] -[SuppressMessage( - "Maintainability", - "CA1515:Consider making public types internal", - Justification = "MSTest requires public test classes.")] public class Tests { /// @@ -81,7 +75,7 @@ public class Tests /// /// Tests that returns the expected result for various inputs. /// -[TestMethod] +[DataTestMethod] [DataRow(, )] [DataRow(, )] public void __( input, expected) @@ -93,7 +87,7 @@ public void __( input, (input); // Assert - result.Should().Be(expected); + Assert.AreEqual(expected, result); } ```