Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,12 @@ The library includes comprehensive testing helpers for consumers:

**Generic Stack Creation**:
- Use `CreateTestStack<TStack>()` and `CreateTestStackMinimal<TStack>()` for custom stack types
- These methods use `Activator.CreateInstance` with reflection to instantiate custom stacks
- Use `CreateTestStack<TStack, TProps>()` and `CreateTestStackMinimal<TStack, TProps>()` 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

Expand All @@ -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
- 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<MyStack>("stack-name", stackProps);
var stack = CdkTestHelper.CreateTestStackMinimal<MyStack>("stack-name", stackProps);
```

**Dual Generic (for custom props interfaces):**
```csharp
// Exact type matching for Activator.CreateInstance
var (app, stack) = CdkTestHelper.CreateTestStack<MyStack, IMyProps>("stack-name", customProps);
var stack = CdkTestHelper.CreateTestStackMinimal<MyStack, IMyProps>("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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
16 changes: 12 additions & 4 deletions src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static (App app, Stack stack) CreateTestStack(string stackName, IStackPro

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TStack">The custom stack type that inherits from Stack</typeparam>
Expand All @@ -63,13 +63,17 @@ public static (App app, TStack stack) CreateTestStack<TStack>(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);
}

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TStack">The custom stack type that inherits from Stack</typeparam>
Expand All @@ -82,7 +86,11 @@ public static (App app, TStack stack) CreateTestStack<TStack, TProps>(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);
}

Expand Down
220 changes: 220 additions & 0 deletions test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestPublicConstructorStack>("test-public-stack", stackProps);

app.Should().NotBeNull();
stack.Should().NotBeNull();
stack.Should().BeOfType<TestPublicConstructorStack>();
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<TestInternalConstructorStack>("test-internal-stack", stackProps);

app.Should().NotBeNull();
stack.Should().NotBeNull();
stack.Should().BeOfType<TestInternalConstructorStack>();
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<TestPublicConstructorStack>("test-public-minimal", stackProps);

stack.Should().NotBeNull();
stack.Should().BeOfType<TestPublicConstructorStack>();
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<TestInternalConstructorStack>("test-internal-minimal", stackProps);

stack.Should().NotBeNull();
stack.Should().BeOfType<TestInternalConstructorStack>();
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<TestInternalConstructorWithCustomPropsStack, TestInternalConstructorStackProps>("test-internal-custom", customProps);

app.Should().NotBeNull();
stack.Should().NotBeNull();
stack.Should().BeOfType<TestInternalConstructorWithCustomPropsStack>();
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<TestInternalConstructorWithCustomPropsStack, TestInternalConstructorStackProps>("test-internal-custom-minimal", customProps);

stack.Should().NotBeNull();
stack.Should().BeOfType<TestInternalConstructorWithCustomPropsStack>();
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<TestInternalConstructorStack>("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<string, string>
{
{ "ENVIRONMENT", "test" },
{ "CONSTRUCTOR_TYPE", "Internal" }
});

stack.IsInternalConstructor.Should().BeTrue();
}
}

// Test helper interface for custom props testing
Expand Down Expand Up @@ -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";
}
}