From 7286c7ce9311676155a8e298768019b00ca2f89e Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Thu, 3 Jul 2025 20:29:35 -0400 Subject: [PATCH] Add dual generic parameter methods to CdkTestHelper for exact type matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add CreateTestStack and CreateTestStackMinimal methods - Solve Activator.CreateInstance type matching issues when custom stack constructors expect specific props interfaces - Add comprehensive unit tests for dual generic methods with TestAdvancedStack and TestCustomStackProps - Update README.md with "Testing with Custom Stack Props Types" section explaining when to use dual generics - Maintain backward compatibility with existing single generic methods 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 35 ++++ .../Testing/CdkTestHelper.cs | 37 +++++ .../Testing/TestingHelpersTests.cs | 149 +++++++++++++++++- 3 files changed, 215 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 81e6270..60e9af3 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,41 @@ public void MyCustomStack_ShouldCreateInfrastructure() - **Clean Tests**: Eliminate boilerplate while maintaining flexibility - **Real-World Ready**: Works with any custom stack implementation +### Example: Testing with Custom Stack Props Types + +For advanced scenarios where your custom stack expects specific props interfaces (not just `IStackProps`), you can use the dual-generic methods: + +```csharp +[Fact] +public void MyAdvancedStack_ShouldWorkWithCustomProps() +{ + // Custom props interface extending IStackProps + var customProps = new MyCustomStackProps + { + Env = new Environment { Account = "123456789012", Region = "us-east-1" }, + Context = new MyContext { DatabaseUrl = "test-db-url" }, + FeatureFlags = new Dictionary { { "EnableNewFeature", true } } + }; + + // Use dual generics for exact type matching - solves Activator.CreateInstance issues + var stack = CdkTestHelper.CreateTestStackMinimal( + "advanced-test", customProps); + + // Your stack gets the exact props type it expects + var template = Template.FromStack(stack); + + // Test with full type safety + template.ShouldHaveLambdaFunction("advanced-function"); + stack.DatabaseUrl.Should().Be("test-db-url"); // Custom props available + stack.IsNewFeatureEnabled.Should().Be(true); +} +``` + +**When to Use Dual Generics:** +- Your stack constructor expects a specific props interface (e.g., `ILightsaberStackProps`, `IInfraStackProps`) +- You need exact type matching for `Activator.CreateInstance` to work correctly +- You want full IntelliSense support for your custom props throughout the test + ### 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 b6a1883..2879748 100644 --- a/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs +++ b/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs @@ -67,6 +67,25 @@ public static (App app, TStack stack) CreateTestStack(string stackName, return (app, stack); } + /// + /// Creates a test CDK app and custom stack type with the specified custom props type. + /// This method uses reflection to instantiate the custom stack type with custom props. + /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. + /// + /// The custom stack type that inherits from Stack + /// The custom props type that implements IStackProps + /// The name of the test stack + /// Custom stack props of type TProps + /// Tuple containing the app and the custom stack instance + public static (App app, TStack stack) CreateTestStack(string stackName, TProps stackProps) + where TStack : Stack + where TProps : IStackProps + { + 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. @@ -113,6 +132,24 @@ public static TStack CreateTestStackMinimal(string stackName, IStackProp return stack; } + /// + /// Creates a test CDK custom stack type with the specified custom props type (app is created internally). + /// This method uses reflection to instantiate the custom stack type with custom props. + /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. + /// + /// The custom stack type that inherits from Stack + /// The custom props type that implements IStackProps + /// The name of the test stack + /// Custom stack props of type TProps + /// The custom stack instance for testing + public static TStack CreateTestStackMinimal(string stackName, TProps stackProps) + where TStack : Stack + where TProps : IStackProps + { + 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 693359f..743d6e1 100644 --- a/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs +++ b/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs @@ -141,8 +141,8 @@ public void PropsBuilder_ShouldAddCustomPolicy() { var customStatement = new Amazon.CDK.AWS.IAM.PolicyStatement(new Amazon.CDK.AWS.IAM.PolicyStatementProps { - Actions = new[] { "sns:Publish" }, - Resources = new[] { "arn:aws:sns:us-east-1:123456789012:my-topic" }, + Actions = ["sns:Publish"], + Resources = ["arn:aws:sns:us-east-1:123456789012:my-topic"], Effect = Amazon.CDK.AWS.IAM.Effect.ALLOW }); @@ -162,7 +162,7 @@ public void PropsBuilder_ShouldAddCustomPolicy() [Fact] public void PropsBuilder_ShouldAddCustomPermission() { - var customPermission = new LayeredCraft.Cdk.Constructs.Models.LambdaPermission + var customPermission = new Models.LambdaPermission { Principal = "events.amazonaws.com", Action = "lambda:InvokeFunction", @@ -316,7 +316,7 @@ public void CdkTestHelper_ShouldCreateGenericStackType() stack.Region.Should().Be("ca-central-1"); // Verify custom stack functionality - var customStack = (TestCustomStack)stack; + var customStack = stack; customStack.CustomProperty.Should().Be("CustomValue"); } @@ -347,7 +347,7 @@ public void CdkTestHelper_ShouldCreateGenericStackTypeMinimal() stack.Region.Should().Be("eu-central-1"); // Verify custom stack functionality - var customStack = (TestCustomStack)stack; + var customStack = stack; customStack.CustomProperty.Should().Be("CustomValue"); } @@ -385,9 +385,131 @@ public void CdkTestHelper_ShouldWorkWithGenericStackAndConstructs() }); // Verify custom stack properties - var customStack = (TestCustomStack)stack; + var customStack = stack; customStack.CustomProperty.Should().Be("CustomValue"); } + + [Fact] + public void CdkTestHelper_ShouldCreateDualGenericStackType() + { + var customProps = new TestCustomStackProps + { + Env = new Amazon.CDK.Environment + { + Account = "222333444555", + Region = "eu-west-2" + }, + Description = "Dual generic stack test", + CustomContext = "test-context", + EnableFeature = true + }; + + // Create stack using dual generics for exact type matching + var (app, stack) = CdkTestHelper.CreateTestStack("dual-generic-stack", customProps); + + app.Should().NotBeNull(); + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("dual-generic-stack"); + stack.Account.Should().Be("222333444555"); + stack.Region.Should().Be("eu-west-2"); + + // Verify custom stack functionality with custom props + stack.CustomContext.Should().Be("test-context"); + stack.FeatureEnabled.Should().Be(true); + stack.ProcessedValue.Should().Be("test-context-processed"); + } + + [Fact] + public void CdkTestHelper_ShouldCreateDualGenericStackTypeMinimal() + { + var customProps = new TestCustomStackProps + { + Env = new Amazon.CDK.Environment + { + Account = "666777888999", + Region = "ap-northeast-1" + }, + CustomContext = "minimal-context", + EnableFeature = false, + Tags = new Dictionary + { + { "TestMethod", "DualGeneric" }, + { "PropsType", "Custom" } + } + }; + + // Create stack using dual generics minimal method + var stack = CdkTestHelper.CreateTestStackMinimal("dual-minimal-stack", customProps); + + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("dual-minimal-stack"); + stack.Account.Should().Be("666777888999"); + stack.Region.Should().Be("ap-northeast-1"); + + // Verify custom props were passed correctly + stack.CustomContext.Should().Be("minimal-context"); + stack.FeatureEnabled.Should().Be(false); + stack.ProcessedValue.Should().Be("minimal-context-processed"); + } + + [Fact] + public void CdkTestHelper_ShouldWorkWithDualGenericStackAndConstructs() + { + var customProps = new TestCustomStackProps + { + Env = new Amazon.CDK.Environment + { + Account = "111222333444", + Region = "us-west-1" + }, + CustomContext = "integration-test", + EnableFeature = true + }; + + // Create custom stack with dual generics and add constructs + var stack = CdkTestHelper.CreateTestStackMinimal("integration-dual", customProps); + + var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithFunctionName("dual-generic-function") + .WithEnvironmentVariable("STACK_CONTEXT", stack.CustomContext) + .WithEnvironmentVariable("FEATURE_ENABLED", stack.FeatureEnabled.ToString()) + .Build(); + + _ = new LambdaFunctionConstruct(stack, "DualGenericConstruct", props); + + // Create template AFTER adding constructs to the stack + var template = Template.FromStack(stack); + + // Verify everything works together with custom props + template.ShouldHaveLambdaFunction("dual-generic-function-test"); + template.ShouldHaveEnvironmentVariables(new Dictionary + { + { "ENVIRONMENT", "test" }, + { "STACK_CONTEXT", "integration-test" }, + { "FEATURE_ENABLED", "True" } + }); + + // Verify custom stack properties from custom props + stack.CustomContext.Should().Be("integration-test"); + stack.FeatureEnabled.Should().Be(true); + stack.ProcessedValue.Should().Be("integration-test-processed"); + } +} + +// Test helper interface for custom props testing +public interface ITestCustomStackProps : Amazon.CDK.IStackProps +{ + string CustomContext { get; } + bool EnableFeature { get; } +} + +// Test helper class for custom props testing +public class TestCustomStackProps : Amazon.CDK.StackProps, ITestCustomStackProps +{ + public string CustomContext { get; set; } = string.Empty; + public bool EnableFeature { get; set; } } // Test helper class for generic stack testing @@ -399,4 +521,19 @@ public TestCustomStack(Amazon.CDK.App scope, string id, Amazon.CDK.IStackProps p { CustomProperty = "CustomValue"; } +} + +// Test helper class for dual generic testing with custom props +public class TestAdvancedStack : Amazon.CDK.Stack +{ + public string CustomContext { get; } + public bool FeatureEnabled { get; } + public string ProcessedValue { get; } + + public TestAdvancedStack(Amazon.CDK.App scope, string id, ITestCustomStackProps props) : base(scope, id, props) + { + CustomContext = props.CustomContext; + FeatureEnabled = props.EnableFeature; + ProcessedValue = $"{props.CustomContext}-processed"; + } } \ No newline at end of file