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
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, bool> { { "EnableNewFeature", true } }
};

// Use dual generics for exact type matching - solves Activator.CreateInstance issues
var stack = CdkTestHelper.CreateTestStackMinimal<MyAdvancedStack, MyCustomStackProps>(
"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:
Expand Down
37 changes: 37 additions & 0 deletions src/LayeredCraft.Cdk.Constructs/Testing/CdkTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ public static (App app, TStack stack) CreateTestStack<TStack>(string stackName,
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.
/// 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>
/// <typeparam name="TProps">The custom props type that implements IStackProps</typeparam>
/// <param name="stackName">The name of the test stack</param>
/// <param name="stackProps">Custom stack props of type TProps</param>
/// <returns>Tuple containing the app and the custom stack instance</returns>
Comment on lines +70 to +79

Copilot AI Jul 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The XML doc comments for the dual-generic overloads largely duplicate those of the single-generic methods; consider refactoring or sharing common documentation to reduce maintenance overhead.

Suggested change
/// <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.
/// 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>
/// <typeparam name="TProps">The custom props type that implements IStackProps</typeparam>
/// <param name="stackName">The name of the test stack</param>
/// <param name="stackProps">Custom stack props of type TProps</param>
/// <returns>Tuple containing the app and the custom stack instance</returns>
/// <inheritdoc cref="CreateTestStack(string, IStackProps)" />
/// <typeparam name="TStack">The custom stack type that inherits from Stack</typeparam>
/// <typeparam name="TProps">The custom props type that implements IStackProps</typeparam>

Copilot uses AI. Check for mistakes.
public static (App app, TStack stack) CreateTestStack<TStack, TProps>(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);
}

/// <summary>
/// Creates a test CDK stack (app is created internally).
/// Note: You must create the Template.FromStack(stack) after adding constructs to the stack.
Expand Down Expand Up @@ -113,6 +132,24 @@ public static TStack CreateTestStackMinimal<TStack>(string stackName, IStackProp
return stack;
}

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TStack">The custom stack type that inherits from Stack</typeparam>
/// <typeparam name="TProps">The custom props type that implements IStackProps</typeparam>
/// <param name="stackName">The name of the test stack</param>
/// <param name="stackProps">Custom stack props of type TProps</param>
/// <returns>The custom stack instance for testing</returns>
public static TStack CreateTestStackMinimal<TStack, TProps>(string stackName, TProps stackProps)
where TStack : Stack
where TProps : IStackProps
{
var (_, stack) = CreateTestStack<TStack, TProps>(stackName, stackProps);
return stack;
}

/// <summary>
/// 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.
Expand Down
149 changes: 143 additions & 6 deletions test/LayeredCraft.Cdk.Constructs.Tests/Testing/TestingHelpersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
});

Expand All @@ -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",
Expand Down Expand Up @@ -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");
Comment on lines +319 to 320

Copilot AI Jul 4, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The alias customStack is redundant since stack is already the correct type; you can assert directly on stack to simplify the test.

Suggested change
var customStack = stack;
customStack.CustomProperty.Should().Be("CustomValue");
stack.CustomProperty.Should().Be("CustomValue");

Copilot uses AI. Check for mistakes.
}

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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<TestAdvancedStack, TestCustomStackProps>("dual-generic-stack", customProps);

app.Should().NotBeNull();
stack.Should().NotBeNull();
stack.Should().BeOfType<TestAdvancedStack>();
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<string, string>
{
{ "TestMethod", "DualGeneric" },
{ "PropsType", "Custom" }
}
};

// Create stack using dual generics minimal method
var stack = CdkTestHelper.CreateTestStackMinimal<TestAdvancedStack, TestCustomStackProps>("dual-minimal-stack", customProps);

stack.Should().NotBeNull();
stack.Should().BeOfType<TestAdvancedStack>();
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<TestAdvancedStack, TestCustomStackProps>("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<string, string>
{
{ "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
Expand All @@ -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";
}
}