From 4ce5575eb606c775c5a4e63970e3e6f2d9adbe32 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Thu, 3 Jul 2025 17:30:21 -0400 Subject: [PATCH] Add generic stack creation methods to CdkTestHelper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Added generic methods to create custom stack types directly without workarounds - Enhanced testing flexibility for projects with custom Stack implementations - Updated documentation with comprehensive examples and usage patterns ## Changes Made ### โœจ New Generic Methods - **`CreateTestStack(string, IStackProps)`**: Creates custom stack types using reflection - **`CreateTestStackMinimal(string, IStackProps)`**: Creates custom stack types with app created internally ### ๐Ÿ”ง Technical Implementation - Uses `Activator.CreateInstance` with reflection for stack instantiation - Generic constraint: `where TStack : Stack` ensures type safety - Returns strongly-typed stack instances for full IntelliSense support ### ๐Ÿงช Comprehensive Test Coverage - **`CdkTestHelper_ShouldCreateGenericStackType`**: Tests full generic method functionality - **`CdkTestHelper_ShouldCreateGenericStackTypeMinimal`**: Tests minimal generic method - **`CdkTestHelper_ShouldWorkWithGenericStackAndConstructs`**: End-to-end integration test - **`TestCustomStack`**: Test helper class demonstrating custom stack implementation ### ๐Ÿ“š Documentation Updates - **README.md**: Added new section "Testing Custom Stack Types" with real-world examples - **CLAUDE.md**: Updated testing helpers documentation and added generic stack creation guidance ## Problem Solved **Before** (workaround required): ```csharp var (app, _) = CdkTestHelper.CreateTestStack("unused-name", props); var infraStack = new InfraStack(app, "real-name", props); // Manual instantiation ``` **After** (direct custom stack creation): ```csharp var infraStack = CdkTestHelper.CreateTestStackMinimal("test-stack", props); ``` ## Benefits โœ… **No Workarounds**: Create custom stack types directly โœ… **Type Safety**: Strongly-typed stack instances with full IntelliSense โœ… **Clean Code**: Eliminates boilerplate while maintaining flexibility โœ… **Real-World Ready**: Works with any custom stack implementation (LightsaberStackProps, etc.) โœ… **Backward Compatible**: All existing code continues to work unchanged ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 10 +- README.md | 36 +++++- .../Testing/CdkTestHelper.cs | 33 ++++++ .../Testing/TestingHelpersTests.cs | 108 ++++++++++++++++++ 4 files changed, 185 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e5ce12d..c95b3aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,9 @@ The library includes comprehensive testing helpers for consumers: **CdkTestHelper** (`CdkTestHelper.cs`): - `CreateTestStack()`: Creates CDK app and stack with test environment -- `CreateTestStackMinimal()`: Creates only the stack (app created internally) +- `CreateTestStack()`: Creates custom stack types directly using generics and reflection +- `CreateTestStackMinimal()`: Creates only the stack (app created internally) +- `CreateTestStackMinimal()`: Creates custom stack types with app created internally - `GetTestAssetPath()`: Resolves test asset paths relative to executing assembly - `CreatePropsBuilder()`: Creates fluent builder with test defaults @@ -105,6 +107,12 @@ The library includes comprehensive testing helpers for consumers: - Always create CDK templates AFTER adding constructs to stacks - Use `Template.FromStack(stack)` after construct creation, not before +**Generic Stack Creation**: +- Use `CreateTestStack()` and `CreateTestStackMinimal()` for custom stack types +- These methods use `Activator.CreateInstance` with reflection to instantiate custom stacks +- Eliminates the need to create unused base stacks when testing custom stack implementations +- Supports any stack type that inherits from `Amazon.CDK.Stack` and follows the standard constructor pattern + ## Development Practices ### Code Style diff --git a/README.md b/README.md index d3bb39a..81e6270 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ The library includes comprehensive testing helpers to make it easy to unit test ### Testing Helpers Overview -- **`CdkTestHelper`**: Reduces boilerplate for creating test stacks and props +- **`CdkTestHelper`**: Reduces boilerplate for creating test stacks and props (including custom stack types) - **`LambdaFunctionConstructAssertions`**: Extension methods for asserting Lambda resources - **`LambdaFunctionConstructPropsBuilder`**: Fluent builder for creating test props @@ -200,6 +200,40 @@ public void MyStack_ShouldCreateLegacyFunction() } ``` +### Example: Testing Custom Stack Types + +For projects with custom stack implementations (inheriting from `Stack`), you can use the generic methods to create your custom stack types directly: + +```csharp +[Fact] +public void MyCustomStack_ShouldCreateInfrastructure() +{ + // Custom stack props (could be your LightsaberStackProps, InfraStackProps, etc.) + var customProps = new MyCustomStackProps + { + Env = new Environment { Account = "123456789012", Region = "us-east-1" }, + Context = new MyContext { /* your custom context */ }, + Tags = new Dictionary { { "Environment", "test" } } + }; + + // Create your custom stack type directly - no unused base stack needed! + var customStack = CdkTestHelper.CreateTestStackMinimal("test-stack", customProps); + + // Your custom stack is ready to use + var template = Template.FromStack(customStack); + + // Verify your custom stack's infrastructure + template.ShouldHaveLambdaFunction("my-function-test"); + // ... other assertions +} +``` + +**Benefits of Generic Stack Creation:** +- **No Workarounds**: Create custom stack types directly instead of creating unused base stacks +- **Type Safety**: Get strongly-typed stack instances with full IntelliSense support +- **Clean Tests**: Eliminate boilerplate while maintaining flexibility +- **Real-World Ready**: Works with any custom stack implementation + ### Test Asset Management Create a `TestAssets` folder in your test project and place your Lambda deployment packages there: diff --git a/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs b/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs index ad81363..b6a1883 100644 --- a/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs +++ b/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs @@ -50,6 +50,23 @@ public static (App app, Stack stack) CreateTestStack(string stackName, IStackPro return (app, stack); } + /// + /// Creates a test CDK app and custom stack type with the specified props. + /// This method uses reflection to instantiate the custom stack type. + /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. + /// + /// The custom stack type that inherits from Stack + /// The name of the test stack + /// Custom stack props implementing IStackProps + /// Tuple containing the app and the custom stack instance + public static (App app, TStack stack) CreateTestStack(string stackName, IStackProps stackProps) + where TStack : Stack + { + var app = new App(); + var stack = (TStack)Activator.CreateInstance(typeof(TStack), app, stackName, stackProps)!; + return (app, stack); + } + /// /// Creates a test CDK stack (app is created internally). /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. @@ -80,6 +97,22 @@ public static Stack CreateTestStackMinimal(string stackName, IStackProps stackPr return stack; } + /// + /// Creates a test CDK custom stack type with the specified props (app is created internally). + /// This method uses reflection to instantiate the custom stack type. + /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. + /// + /// The custom stack type that inherits from Stack + /// The name of the test stack + /// Custom stack props implementing IStackProps + /// The custom stack instance for testing + public static TStack CreateTestStackMinimal(string stackName, IStackProps stackProps) + where TStack : Stack + { + var (_, stack) = CreateTestStack(stackName, stackProps); + return stack; + } + /// /// Gets the path to a test asset relative to the executing assembly location. /// This ensures test assets can be found regardless of the current working directory. diff --git a/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs b/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs index 0e21015..693359f 100644 --- a/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs +++ b/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs @@ -291,4 +291,112 @@ public void CdkTestHelper_ShouldWorkWithRealWorldCustomStackProps() template.ShouldHaveLambdaFunction("real-world-function-test"); } + + [Fact] + public void CdkTestHelper_ShouldCreateGenericStackType() + { + var customProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "111222333444", + Region = "ca-central-1" + }, + Description = "Generic stack test" + }; + + // Create a custom stack type using the generic method + var (app, stack) = CdkTestHelper.CreateTestStack("generic-test-stack", customProps); + + app.Should().NotBeNull(); + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("generic-test-stack"); + stack.Account.Should().Be("111222333444"); + stack.Region.Should().Be("ca-central-1"); + + // Verify custom stack functionality + var customStack = (TestCustomStack)stack; + customStack.CustomProperty.Should().Be("CustomValue"); + } + + [Fact] + public void CdkTestHelper_ShouldCreateGenericStackTypeMinimal() + { + var customProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "555777999111", + Region = "eu-central-1" + }, + Tags = new Dictionary + { + { "TestType", "Generic" }, + { "Framework", "CDK" } + } + }; + + // Create custom stack type using the minimal generic method + var stack = CdkTestHelper.CreateTestStackMinimal("minimal-generic-stack", customProps); + + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("minimal-generic-stack"); + stack.Account.Should().Be("555777999111"); + stack.Region.Should().Be("eu-central-1"); + + // Verify custom stack functionality + var customStack = (TestCustomStack)stack; + customStack.CustomProperty.Should().Be("CustomValue"); + } + + [Fact] + public void CdkTestHelper_ShouldWorkWithGenericStackAndConstructs() + { + var customProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "888999000111", + Region = "us-east-2" + } + }; + + // Create custom stack and add constructs to it + var stack = CdkTestHelper.CreateTestStackMinimal("integration-test", customProps); + + var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithFunctionName("generic-function") + .WithEnvironmentVariable("STACK_TYPE", "CustomStack") + .Build(); + + _ = new LambdaFunctionConstruct(stack, "GenericConstruct", props); + + // Create template AFTER adding constructs to the stack + var template = Template.FromStack(stack); + + // Verify everything works together + template.ShouldHaveLambdaFunction("generic-function-test"); + template.ShouldHaveEnvironmentVariables(new Dictionary + { + { "ENVIRONMENT", "test" }, + { "STACK_TYPE", "CustomStack" } + }); + + // Verify custom stack properties + var customStack = (TestCustomStack)stack; + customStack.CustomProperty.Should().Be("CustomValue"); + } +} + +// Test helper class for generic stack testing +public class TestCustomStack : Amazon.CDK.Stack +{ + public string CustomProperty { get; } + + public TestCustomStack(Amazon.CDK.App scope, string id, Amazon.CDK.IStackProps props) : base(scope, id, props) + { + CustomProperty = "CustomValue"; + } } \ No newline at end of file