diff --git a/AGENTS.md b/AGENTS.md index 4506302..f54c4f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ implementation demonstrating best practices for DEMA Consulting .NET libraries. ## Key Files - **`requirements.yaml`** - All requirements with test linkage (enforced via `dotnet reqstream --enforce`) -- **`.editorconfig`** - Code style (file-scoped namespaces, 4-space indent, UTF-8+BOM, LF endings) +- **`.editorconfig`** - Code style (file-scoped namespaces, 4-space indent, UTF-8, LF endings) - **`.cspell.json`, `.markdownlint-cli2.jsonc`, `.yamllint.yaml`** - Linting configs ## Requirements diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef6e8f2..6062608 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ This project enforces code style through `.editorconfig`. Key requirements: - **Indentation**: 4 spaces for C#, 2 spaces for YAML/JSON/XML - **Line Endings**: LF (Unix-style) -- **Encoding**: UTF-8 with BOM +- **Encoding**: UTF-8 - **Namespaces**: Use file-scoped namespace declarations - **Braces**: Required for all control statements - **Naming Conventions**: @@ -142,7 +142,7 @@ Examples: - Write tests that are clear and focused - Use modern MSTest v4 assertions: - - `Assert.HasCount(collection, expectedCount)` + - `Assert.HasCount(expectedCount, collection)` - `Assert.IsEmpty(collection)` - `Assert.DoesNotContain(item, collection)` - Always clean up resources (use `try/finally` for console redirection) diff --git a/README.md b/README.md index 92e91c7..51c802e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This template demonstrates: - **Simple Library Structure**: Demo class with example methods - **Multi-Platform Support**: Builds and runs on Windows and Linux -- **Multi-Runtime Support**: Targets .NET 8, 9, and 10 +- **Multi-Runtime Support**: Targets .NET Standard 2.0, .NET 8, 9, and 10 - **MSTest V4**: Modern unit testing with MSTest framework version 4 - **Comprehensive CI/CD**: GitHub Actions workflows with quality checks and builds - **Documentation Generation**: Automated build notes, user guide, code quality reports, diff --git a/docs/guide/guide.md b/docs/guide/guide.md index 71dbf6d..cb199f8 100644 --- a/docs/guide/guide.md +++ b/docs/guide/guide.md @@ -39,6 +39,16 @@ Console.WriteLine(result); // Output: Hello, World! The `DemoClass` provides demonstration functionality for the template library. +#### Constants + +##### DefaultPrefix + +```csharp +public const string DefaultPrefix = "Hello"; +``` + +The greeting prefix used when no custom prefix is specified. + #### Constructors ##### DemoClass() @@ -59,11 +69,22 @@ Initializes a new instance of the `DemoClass` class with a custom prefix. **Parameters:** -- `prefix` (string): The prefix to use in greetings. Must not be null. +- `prefix` (string): The prefix to use in greetings. Must not be null or empty. **Exceptions:** - `ArgumentNullException`: Thrown when `prefix` is null. +- `ArgumentException`: Thrown when `prefix` is an empty string. + +#### Properties + +##### Prefix + +```csharp +public string Prefix { get; } +``` + +Gets the greeting prefix used by this instance. #### Methods @@ -77,7 +98,7 @@ Returns a greeting message for the specified name. **Parameters:** -- `name` (string): The name to greet. Must not be null. +- `name` (string): The name to greet. Must not be null or empty. **Returns:** @@ -86,6 +107,7 @@ A string containing the greeting message in the format "{prefix}, {name}!". **Exceptions:** - `ArgumentNullException`: Thrown when `name` is null. +- `ArgumentException`: Thrown when `name` is an empty string. **Example:** diff --git a/requirements.yaml b/requirements.yaml index c205cce..1ed0922 100644 --- a/requirements.yaml +++ b/requirements.yaml @@ -21,6 +21,30 @@ sections: .NET library best practices for DEMA Consulting libraries. tests: - DemoMethod_ReturnsGreeting_WithDefaultPrefix + - DemoMethod_ReturnsGreeting_WithCustomPrefix + + - id: TMPL-REQ-008 + title: The library shall support a customizable greeting prefix. + justification: | + Consumers may need to produce greetings with a prefix other than the + default, so the library must allow the prefix to be specified at + construction time. + tests: + - DemoMethod_ReturnsGreeting_WithCustomPrefix + + - id: TMPL-REQ-009 + title: The library shall reject null and empty arguments with an appropriate ArgumentException. + justification: | + Null-safety and non-empty validation are fundamental API contracts: passing null or an + empty string to the constructor or to DemoMethod is a programming error. Throwing + ArgumentNullException for null arguments and ArgumentException for empty-string arguments + immediately surfaces the mistake to callers and prevents malformed output (such as + ", World!" or "Hello, !") and obscure failures deeper in the call stack. + tests: + - DemoMethod_ThrowsArgumentNullException_ForNullInput + - DemoMethod_ThrowsArgumentException_ForEmptyInput + - Constructor_ThrowsArgumentNullException_ForNullPrefix + - Constructor_ThrowsArgumentException_ForEmptyPrefix - title: Platform Support requirements: diff --git a/src/TemplateDotNetLibrary/DemoClass.cs b/src/TemplateDotNetLibrary/DemoClass.cs index 0035a85..3dd0524 100644 --- a/src/TemplateDotNetLibrary/DemoClass.cs +++ b/src/TemplateDotNetLibrary/DemoClass.cs @@ -5,13 +5,24 @@ namespace TemplateDotNetLibrary; /// public class DemoClass { + /// + /// The greeting prefix used when no custom prefix is specified. + /// + public const string DefaultPrefix = "Hello"; + + /// + /// The prefix prepended to every greeting produced by this instance. + /// private readonly string _prefix; /// - /// Initializes a new instance of the class with default prefix. + /// Initializes a new instance of the class with the default prefix. /// + /// + /// The prefix is set to . + /// public DemoClass() - : this("Hello") + : this(DefaultPrefix) { } @@ -19,20 +30,44 @@ public DemoClass() /// Initializes a new instance of the class with a custom prefix. /// /// The prefix to use in greetings. + /// + /// Thrown when is . + /// + /// + /// Thrown when is an empty string. + /// public DemoClass(string prefix) { - ArgumentNullException.ThrowIfNull(prefix); + // Validate that the prefix is non-null and non-empty before storing it + ArgumentException.ThrowIfNullOrEmpty(prefix); _prefix = prefix; } /// - /// Demo method that returns a greeting message. + /// Gets the greeting prefix used by this instance. /// - /// The name to greet. - /// A greeting message. + public string Prefix => _prefix; + + /// + /// Returns a greeting message that combines the instance prefix with the given name. + /// + /// The name to include in the greeting. + /// + /// A greeting string in the format {prefix}, {name}!, + /// for example Hello, World!. + /// + /// + /// Thrown when is . + /// + /// + /// Thrown when is an empty string. + /// public string DemoMethod(string name) { - ArgumentNullException.ThrowIfNull(name); + // Validate that the name is non-null and non-empty before building the greeting + ArgumentException.ThrowIfNullOrEmpty(name); + + // Combine the prefix and name into the standard greeting format return $"{_prefix}, {name}!"; } } diff --git a/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs b/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs index 389b915..c70659b 100644 --- a/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs +++ b/test/TemplateDotNetLibrary.Tests/DemoClassTests.cs @@ -7,7 +7,8 @@ namespace TemplateDotNetLibrary.Tests; public class DemoClassTests { /// - /// Test that DemoMethod returns the expected greeting with default prefix. + /// Proves that DemoMethod produces the expected "{prefix}, {name}!" format + /// when the default constructor is used. /// [TestMethod] public void DemoMethod_ReturnsGreeting_WithDefaultPrefix() @@ -19,12 +20,13 @@ public void DemoMethod_ReturnsGreeting_WithDefaultPrefix() // Act var result = demo.DemoMethod(name); - // Assert + // Assert – greeting must be exactly "Hello, World!" Assert.AreEqual("Hello, World!", result); } /// - /// Test that DemoMethod returns the expected greeting with custom prefix. + /// Proves that DemoMethod uses the custom prefix supplied at construction + /// time instead of the default. /// [TestMethod] public void DemoMethod_ReturnsGreeting_WithCustomPrefix() @@ -36,12 +38,13 @@ public void DemoMethod_ReturnsGreeting_WithCustomPrefix() // Act var result = demo.DemoMethod(name); - // Assert + // Assert – greeting must use the custom prefix "Hi" Assert.AreEqual("Hi, Alice!", result); } /// - /// Test that DemoMethod throws ArgumentNullException for null input. + /// Proves that DemoMethod throws ArgumentNullException (not a base + /// ArgumentException) when a null name is supplied. /// [TestMethod] public void DemoMethod_ThrowsArgumentNullException_ForNullInput() @@ -49,17 +52,95 @@ public void DemoMethod_ThrowsArgumentNullException_ForNullInput() // Arrange var demo = new DemoClass(); - // Act & Assert - _ = Assert.Throws(() => demo.DemoMethod(null!)); + // Act & Assert – exact exception type proves null is explicitly rejected + Assert.ThrowsExactly(() => demo.DemoMethod(null!)); } /// - /// Test that constructor throws ArgumentNullException for null prefix. + /// Proves that DemoMethod throws ArgumentException (not ArgumentNullException) + /// when an empty string name is supplied. + /// + [TestMethod] + public void DemoMethod_ThrowsArgumentException_ForEmptyInput() + { + // Arrange + var demo = new DemoClass(); + + // Act & Assert – ArgumentException (not the null sub-type) must be thrown + Assert.ThrowsExactly(() => demo.DemoMethod(string.Empty)); + } + + /// + /// Proves that the custom-prefix constructor throws ArgumentNullException + /// (not a base ArgumentException) when a null prefix is supplied. /// [TestMethod] public void Constructor_ThrowsArgumentNullException_ForNullPrefix() { - // Act & Assert - _ = Assert.Throws(() => new DemoClass(null!)); + // Act & Assert – exact exception type proves null is explicitly rejected + Assert.ThrowsExactly(() => new DemoClass(null!)); + } + + /// + /// Proves that the custom-prefix constructor throws ArgumentException + /// (not ArgumentNullException) when an empty string prefix is supplied. + /// + [TestMethod] + public void Constructor_ThrowsArgumentException_ForEmptyPrefix() + { + // Act & Assert – ArgumentException (not the null sub-type) must be thrown + Assert.ThrowsExactly(() => new DemoClass(string.Empty)); + } + + /// + /// Proves that the DefaultPrefix constant exposes the value "Hello", + /// which is the expected default greeting prefix. + /// + [TestMethod] + public void DemoClass_DefaultPrefix_IsHello() + { + // Arrange + const string expected = "Hello"; + + // Act – read the public constant directly + var actual = DemoClass.DefaultPrefix; + + // Assert – constant value must not silently change + Assert.AreEqual(expected, actual); + } + + /// + /// Proves that the Prefix property returns the custom value provided to + /// the constructor, confirming the property reflects what was stored. + /// + [TestMethod] + public void DemoClass_Prefix_ReturnsCustomPrefix() + { + // Arrange + const string customPrefix = "Greetings"; + var demo = new DemoClass(customPrefix); + + // Act + var actual = demo.Prefix; + + // Assert – Prefix must exactly match the value passed at construction + Assert.AreEqual(customPrefix, actual); + } + + /// + /// Proves that the default constructor stores DefaultPrefix in the Prefix + /// property, tying the constant and the property together explicitly. + /// + [TestMethod] + public void DemoClass_DefaultConstructor_SetsDefaultPrefix() + { + // Arrange + var demo = new DemoClass(); + + // Act + var actual = demo.Prefix; + + // Assert – default constructor must yield exactly DemoClass.DefaultPrefix + Assert.AreEqual(DemoClass.DefaultPrefix, actual); } }