diff --git a/CLAUDE.md b/CLAUDE.md index c95b3aa..125d3e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,9 +109,12 @@ The library includes comprehensive testing helpers for consumers: **Generic Stack Creation**: - Use `CreateTestStack()` and `CreateTestStackMinimal()` for custom stack types -- These methods use `Activator.CreateInstance` with reflection to instantiate custom stacks +- Use `CreateTestStack()` and `CreateTestStackMinimal()` for custom props types +- These methods use `Activator.CreateInstance` with `BindingFlags.NonPublic` to support internal constructors - 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 +- **Constructor Support**: Works with both public and internal constructors (CDK default is internal) +- **Dual Generics**: Use dual generics when your stack constructor expects specific props interfaces ## Development Practices @@ -131,4 +134,36 @@ The library includes comprehensive testing helpers for consumers: - Uses `PROVIDED_AL2023` runtime for Lambda functions (supports custom runtimes) - Hardcoded OpenTelemetry layer ARN for us-east-1 region - Default memory: 1024MB, timeout: 6 seconds, log retention: 2 weeks -- Uses `RemovalPolicy.RETAIN` for Lambda versions to prevent deletion \ No newline at end of file +- Uses `RemovalPolicy.RETAIN` for Lambda versions to prevent deletion + +## Testing Patterns + +### CDK Infrastructure Testing + +The package provides comprehensive testing helpers with support for: +- **Custom stack types** with public or internal constructors +- **Custom props interfaces** that extend IStackProps +- **Automatic test asset** path resolution +- **Fluent assertion methods** for common AWS resources + +### Generic Stack Creation Methods + +**Single Generic (for IStackProps):** +```csharp +// Works with both public and internal constructors +var (app, stack) = CdkTestHelper.CreateTestStack("stack-name", stackProps); +var stack = CdkTestHelper.CreateTestStackMinimal("stack-name", stackProps); +``` + +**Dual Generic (for custom props interfaces):** +```csharp +// Exact type matching for Activator.CreateInstance +var (app, stack) = CdkTestHelper.CreateTestStack("stack-name", customProps); +var stack = CdkTestHelper.CreateTestStackMinimal("stack-name", customProps); +``` + +**Important Implementation Notes:** +- Methods use `BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic` to access internal constructors +- This follows standard testing library patterns for accessing non-public members +- CDK's default pattern is internal constructors, so this support is essential for real-world testing +- The reflection approach ensures compatibility with any custom stack implementation \ No newline at end of file diff --git a/README.md b/README.md index 60e9af3..bb8fed5 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,7 @@ public void MyCustomStack_ShouldCreateInfrastructure() - **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 +- **Constructor Support**: Supports both public and internal constructors (CDK default) ### Example: Testing with Custom Stack Props Types @@ -269,6 +270,35 @@ public void MyAdvancedStack_ShouldWorkWithCustomProps() - You need exact type matching for `Activator.CreateInstance` to work correctly - You want full IntelliSense support for your custom props throughout the test +### Supported Constructor Patterns + +The generic methods work with both public and internal constructors using reflection with `BindingFlags`: + +```csharp +// ✅ Works - Public constructor +public class MyStack : Stack +{ + public MyStack(Construct scope, string id, IStackProps props) : base(scope, id, props) { } +} + +// ✅ Works - Internal constructor (CDK default pattern) +public class MyStack : Stack +{ + internal MyStack(Construct scope, string id, IStackProps props) : base(scope, id, props) { } +} + +// ✅ Works - Custom props interface with internal constructor +public class MyStack : Stack +{ + internal MyStack(Construct scope, string id, IMyCustomProps props) : base(scope, id, props) { } +} +``` + +**Important Notes:** +- The helpers use `BindingFlags.NonPublic` to access internal constructors +- This follows common testing library patterns for accessing non-public members +- Works seamlessly with CDK's default internal constructor pattern + ### 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 2879748..42a46c0 100644 --- a/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs +++ b/src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs @@ -52,7 +52,7 @@ public static (App app, Stack stack) CreateTestStack(string stackName, IStackPro /// /// Creates a test CDK app and custom stack type with the specified props. - /// This method uses reflection to instantiate the custom stack type. + /// This method uses reflection to instantiate the custom stack type, supporting both public and internal constructors. /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. /// /// The custom stack type that inherits from Stack @@ -63,13 +63,17 @@ public static (App app, TStack stack) CreateTestStack(string stackName, where TStack : Stack { var app = new App(); - var stack = (TStack)Activator.CreateInstance(typeof(TStack), app, stackName, stackProps)!; + var stack = (TStack)Activator.CreateInstance(typeof(TStack), + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, + [app, stackName, stackProps], + null)!; 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. + /// This method uses reflection to instantiate the custom stack type, supporting both public and internal constructors. /// Note: You must create the Template.FromStack(stack) after adding constructs to the stack. /// /// The custom stack type that inherits from Stack @@ -82,7 +86,11 @@ public static (App app, TStack stack) CreateTestStack(string sta where TProps : IStackProps { var app = new App(); - var stack = (TStack)Activator.CreateInstance(typeof(TStack), app, stackName, stackProps)!; + var stack = (TStack)Activator.CreateInstance(typeof(TStack), + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, + [app, stackName, stackProps], + null)!; return (app, stack); } diff --git a/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs b/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs index 743d6e1..9ebd239 100644 --- a/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs +++ b/test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs @@ -496,6 +496,179 @@ public void CdkTestHelper_ShouldWorkWithDualGenericStackAndConstructs() stack.FeatureEnabled.Should().Be(true); stack.ProcessedValue.Should().Be("integration-test-processed"); } + + [Fact] + public void CdkTestHelper_ShouldCreateStackWithPublicConstructor() + { + var stackProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "123456789012", + Region = "us-east-1" + } + }; + + var (app, stack) = CdkTestHelper.CreateTestStack("test-public-stack", stackProps); + + app.Should().NotBeNull(); + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("test-public-stack"); + stack.Account.Should().Be("123456789012"); + stack.Region.Should().Be("us-east-1"); + stack.IsPublicConstructor.Should().BeTrue(); + } + + [Fact] + public void CdkTestHelper_ShouldCreateStackWithInternalConstructor() + { + var stackProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "987654321098", + Region = "eu-west-1" + } + }; + + var (app, stack) = CdkTestHelper.CreateTestStack("test-internal-stack", stackProps); + + app.Should().NotBeNull(); + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("test-internal-stack"); + stack.Account.Should().Be("987654321098"); + stack.Region.Should().Be("eu-west-1"); + stack.IsInternalConstructor.Should().BeTrue(); + } + + [Fact] + public void CdkTestHelper_ShouldCreateStackWithPublicConstructorMinimal() + { + var stackProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "111222333444", + Region = "ap-southeast-2" + } + }; + + var stack = CdkTestHelper.CreateTestStackMinimal("test-public-minimal", stackProps); + + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("test-public-minimal"); + stack.Account.Should().Be("111222333444"); + stack.Region.Should().Be("ap-southeast-2"); + stack.IsPublicConstructor.Should().BeTrue(); + } + + [Fact] + public void CdkTestHelper_ShouldCreateStackWithInternalConstructorMinimal() + { + var stackProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "555666777888", + Region = "ca-central-1" + } + }; + + var stack = CdkTestHelper.CreateTestStackMinimal("test-internal-minimal", stackProps); + + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("test-internal-minimal"); + stack.Account.Should().Be("555666777888"); + stack.Region.Should().Be("ca-central-1"); + stack.IsInternalConstructor.Should().BeTrue(); + } + + [Fact] + public void CdkTestHelper_ShouldCreateStackWithInternalConstructorAndCustomProps() + { + var customProps = new TestInternalConstructorStackProps + { + Env = new Amazon.CDK.Environment + { + Account = "999888777666", + Region = "us-west-2" + }, + InternalValue = "internal-test-value" + }; + + var (app, stack) = CdkTestHelper.CreateTestStack("test-internal-custom", customProps); + + app.Should().NotBeNull(); + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("test-internal-custom"); + stack.Account.Should().Be("999888777666"); + stack.Region.Should().Be("us-west-2"); + stack.InternalValue.Should().Be("internal-test-value"); + stack.ProcessedInternalValue.Should().Be("internal-test-value-processed"); + } + + [Fact] + public void CdkTestHelper_ShouldCreateStackWithInternalConstructorAndCustomPropsMinimal() + { + var customProps = new TestInternalConstructorStackProps + { + Env = new Amazon.CDK.Environment + { + Account = "444333222111", + Region = "ap-northeast-1" + }, + InternalValue = "minimal-internal-value" + }; + + var stack = CdkTestHelper.CreateTestStackMinimal("test-internal-custom-minimal", customProps); + + stack.Should().NotBeNull(); + stack.Should().BeOfType(); + stack.StackName.Should().Be("test-internal-custom-minimal"); + stack.Account.Should().Be("444333222111"); + stack.Region.Should().Be("ap-northeast-1"); + stack.InternalValue.Should().Be("minimal-internal-value"); + stack.ProcessedInternalValue.Should().Be("minimal-internal-value-processed"); + } + + [Fact] + public void CdkTestHelper_ShouldWorkWithInternalConstructorAndConstructs() + { + var stackProps = new Amazon.CDK.StackProps + { + Env = new Amazon.CDK.Environment + { + Account = "777666555444", + Region = "eu-central-1" + } + }; + + var stack = CdkTestHelper.CreateTestStackMinimal("test-internal-with-constructs", stackProps); + + var props = CdkTestHelper.CreatePropsBuilder(AssetPathExtensions.GetTestLambdaZipPath()) + .WithFunctionName("internal-constructor-function") + .WithEnvironmentVariable("CONSTRUCTOR_TYPE", "Internal") + .Build(); + + _ = new LambdaFunctionConstruct(stack, "InternalConstructorConstruct", props); + + // Create template AFTER adding constructs to the stack + var template = Template.FromStack(stack); + + template.ShouldHaveLambdaFunction("internal-constructor-function-test"); + template.ShouldHaveEnvironmentVariables(new Dictionary + { + { "ENVIRONMENT", "test" }, + { "CONSTRUCTOR_TYPE", "Internal" } + }); + + stack.IsInternalConstructor.Should().BeTrue(); + } } // Test helper interface for custom props testing @@ -536,4 +709,51 @@ public TestAdvancedStack(Amazon.CDK.App scope, string id, ITestCustomStackProps FeatureEnabled = props.EnableFeature; ProcessedValue = $"{props.CustomContext}-processed"; } +} + +// Test helper class for public constructor testing +public class TestPublicConstructorStack : Amazon.CDK.Stack +{ + public bool IsPublicConstructor { get; } + + public TestPublicConstructorStack(Amazon.CDK.App scope, string id, Amazon.CDK.IStackProps props) : base(scope, id, props) + { + IsPublicConstructor = true; + } +} + +// Test helper class for internal constructor testing +public class TestInternalConstructorStack : Amazon.CDK.Stack +{ + public bool IsInternalConstructor { get; } + + internal TestInternalConstructorStack(Amazon.CDK.App scope, string id, Amazon.CDK.IStackProps props) : base(scope, id, props) + { + IsInternalConstructor = true; + } +} + +// Test helper interface for internal constructor with custom props +public interface ITestInternalConstructorStackProps : Amazon.CDK.IStackProps +{ + string InternalValue { get; } +} + +// Test helper class for internal constructor with custom props +public class TestInternalConstructorStackProps : Amazon.CDK.StackProps, ITestInternalConstructorStackProps +{ + public string InternalValue { get; set; } = string.Empty; +} + +// Test helper class for internal constructor with custom props +public class TestInternalConstructorWithCustomPropsStack : Amazon.CDK.Stack +{ + public string InternalValue { get; } + public string ProcessedInternalValue { get; } + + internal TestInternalConstructorWithCustomPropsStack(Amazon.CDK.App scope, string id, ITestInternalConstructorStackProps props) : base(scope, id, props) + { + InternalValue = props.InternalValue; + ProcessedInternalValue = $"{props.InternalValue}-processed"; + } } \ No newline at end of file