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);
}
```