Cloud Runner¶
The Durable Execution SDK Test Cloud Runner invokes a deployed Lambda function and polls for completion. Use it to validate IAM permissions, real service integrations, and deployment correctness. The same test assertions you write for local testing work with the cloud runner.
Set up the cloud runner¶
Create a cloud test runner with the qualified function name. The runner's Lambda client must target the Region the function runs in. The runner invokes the function, extracts the execution ARN from the response, and polls until the execution completes.
import { CloudDurableTestRunner } from "@aws/durable-execution-sdk-js-testing";
import { LambdaClient } from "@aws-sdk/client-lambda";
import { ExecutionStatus } from "@aws-sdk/client-lambda";
const runner = new CloudDurableTestRunner({
functionName: "MyFunction:$LATEST",
client: new LambdaClient({ region: "us-east-1" }),
});
it("runs against a deployed function", async () => {
const result = await runner.run({ payload: { name: "world" } });
expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED);
expect(result.getResult()).toBe("hello world");
});
from aws_durable_execution_sdk_python.execution import InvocationStatus
from aws_durable_execution_sdk_python_testing.runner import DurableFunctionCloudTestRunner
def test_runs_against_deployed_function():
runner = DurableFunctionCloudTestRunner(
function_name="MyFunction:$LATEST",
region="us-east-1",
)
result = runner.run(input='{"name": "world"}', timeout=60)
assert result.status is InvocationStatus.SUCCEEDED
assert result.result is not None
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import software.amazon.lambda.durable.model.ExecutionStatus;
import software.amazon.lambda.durable.testing.CloudDurableTestRunner;
class CloudRunnerTest {
@Test
void runsAgainstDeployedFunction() {
var runner = CloudDurableTestRunner.create(
"arn:aws:lambda:us-east-1:123456789012:function:MyFunction:$LATEST",
String.class,
String.class
);
var result = runner.runUntilComplete("{\"name\": \"world\"}");
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
assertNotNull(result.getResult(String.class));
}
}
Pass durabletest.NewCloudRunner a Lambda client and the qualified function name.
config.LoadDefaultConfig reads the Region and the credentials from the default
chain, such as AWS_REGION and ~/.aws/config. Save this as
cloud_runner_test.go.
package main
import (
"context"
"testing"
"github.com/aws/aws-durable-execution-sdk-go/durable/durabletest"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/lambda"
)
func TestGreeterInCloud(t *testing.T) {
cfg, err := awsconfig.LoadDefaultConfig(context.Background())
if err != nil {
t.Fatalf("load AWS config: %v", err)
}
// The function name needs a qualifier: a version, an alias, or $LATEST.
runner := durabletest.NewCloudRunner(lambda.NewFromConfig(cfg), "MyFunction:$LATEST")
result, err := runner.Run(context.Background(), map[string]any{"name": "world"})
if err != nil {
t.Fatalf("run: %v", err)
}
if result.Status != durabletest.Succeeded {
t.Fatalf("status = %s, want SUCCEEDED", result.Status)
}
greeting, err := durabletest.ResultAs[string](result)
if err != nil {
t.Fatalf("result: %v", err)
}
if greeting != "hello world" {
t.Fatalf("result = %q, want %q", greeting, "hello world")
}
}
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.DurableExecution.Testing;
using Xunit;
public class CloudRunnerTest
{
// The workflow under test. In cloud mode it runs in the deployed function;
// it is shown here so the example is self-contained.
private static async Task<string> Workflow(GreetInput input, IDurableContext ctx)
{
return await ctx.StepAsync(
async (_, _) => $"hello {input.Name}",
name: "greet");
}
[Fact]
public async Task RunsAgainstDeployedFunction()
{
await using var runner = new CloudDurableTestRunner<GreetInput, string>(
"arn:aws:lambda:us-east-1:123456789012:function:MyFunction:$LATEST");
TestResult<string> result = await runner.RunAsync(new GreetInput("world"));
Assert.True(result.IsSucceeded);
Assert.Equal("hello world", result.Result);
}
}
public record GreetInput(string Name);
Deploy before running¶
Cloud mode requires a deployed function. Deploy with your preferred tool before running cloud tests:
After deployment, get the qualified function name or ARN:
aws lambda get-function --function-name MyFunction \
--query 'Configuration.FunctionArn' --output text
Configure polling¶
The runner polls for completion, then retrieves the full operation history for assertions. Configure the poll interval to control how frequently the runner checks for completion.
Pass config: { pollInterval } (in milliseconds) to the constructor:
import { CloudDurableTestRunner } from "@aws/durable-execution-sdk-js-testing";
import { LambdaClient } from "@aws-sdk/client-lambda";
import { ExecutionStatus } from "@aws-sdk/client-lambda";
const runner = new CloudDurableTestRunner({
functionName: "MyFunction:$LATEST",
client: new LambdaClient({ region: "us-east-1" }),
config: { pollInterval: 2000 },
});
it("runs against a deployed function", async () => {
const result = await runner.run({ payload: { name: "world" } });
expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED);
});
Pass timeout in seconds to run():
from aws_durable_execution_sdk_python.execution import InvocationStatus
from aws_durable_execution_sdk_python_testing.runner import DurableFunctionCloudTestRunner
def test_runs_with_custom_timeout():
runner = DurableFunctionCloudTestRunner(
function_name="MyFunction:$LATEST",
region="us-east-1",
)
result = runner.run(input='{"name": "world"}', timeout=60)
assert result.status is InvocationStatus.SUCCEEDED
Use withTimeout(Duration) to set the maximum wait time:
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import software.amazon.lambda.durable.model.ExecutionStatus;
import software.amazon.lambda.durable.testing.CloudDurableTestRunner;
class CloudRunnerTimeoutTest {
@Test
void runsWithCustomTimeout() {
var runner = CloudDurableTestRunner
.create(
"arn:aws:lambda:us-east-1:123456789012:function:MyFunction:$LATEST",
String.class,
String.class
)
.withTimeout(Duration.ofSeconds(60));
var result = runner.runUntilComplete("{\"name\": \"world\"}");
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
}
}
Pass durabletest.WithPollInterval and durabletest.WithTimeout to
NewCloudRunner. The defaults are 2 seconds and 5 minutes. When polling
exceeds the timeout, Run returns an error. To bound a single call, pass
Run a context with a deadline.
package main
import (
"context"
"testing"
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable/durabletest"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/lambda"
)
func TestGreeterPolling(t *testing.T) {
cfg, err := awsconfig.LoadDefaultConfig(context.Background())
if err != nil {
t.Fatalf("load AWS config: %v", err)
}
runner := durabletest.NewCloudRunner(lambda.NewFromConfig(cfg), "MyFunction:$LATEST",
durabletest.WithPollInterval(2*time.Second),
durabletest.WithTimeout(60*time.Second))
result, err := runner.Run(context.Background(), map[string]any{"name": "world"})
if err != nil {
t.Fatalf("run: %v", err)
}
if result.Status != durabletest.Succeeded {
t.Fatalf("status = %s, want SUCCEEDED", result.Status)
}
}
Set DefaultTimeout on CloudTestRunnerOptions, or pass a timeout to RunAsync:
using Amazon.Lambda.DurableExecution;
using Amazon.Lambda.DurableExecution.Testing;
using Xunit;
public class CloudRunnerTimeoutTest
{
private static async Task<string> Workflow(GreetInput input, IDurableContext ctx)
{
return await ctx.StepAsync(
async (_, _) => $"hello {input.Name}",
name: "greet");
}
[Fact]
public async Task RunsWithCustomTimeout()
{
// Set the default wall-clock timeout for polling via CloudTestRunnerOptions,
// and tune how frequently the runner polls for completion.
var options = new CloudTestRunnerOptions
{
InitialPollInterval = TimeSpan.FromMilliseconds(200),
PollInterval = TimeSpan.FromSeconds(2),
DefaultTimeout = TimeSpan.FromSeconds(60),
};
await using var runner = new CloudDurableTestRunner<GreetInput, string>(
"arn:aws:lambda:us-east-1:123456789012:function:MyFunction:$LATEST",
options: options);
// Or override the timeout per call by passing it to RunAsync.
TestResult<string> result = await runner.RunAsync(
new GreetInput("world"),
timeout: TimeSpan.FromSeconds(60));
Assert.True(result.IsSucceeded);
}
}
public record GreetInput(string Name);
Required IAM permissions¶
Cloud mode requires AWS credentials in the environment. The runner uses the default credential chain, so any standard AWS credential configuration works.
The IAM principal running the tests needs these permissions on the target function:
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction",
"lambda:GetDurableExecution",
"lambda:GetDurableExecutionHistory"
],
"Resource": "arn:aws:lambda:region:account-id:function:function-name:*"
}
Troubleshooting¶
Credentials not configured¶
The runner throws when it cannot find AWS credentials. Run aws sts get-caller-identity
to verify your credentials are configured.
Function not found¶
Verify the function name or ARN is correct and the function exists in the target region:
aws lambda get-function --function-name MyFunction.
Execution timeout¶
The function took longer than the configured timeout. Increase the timeout or check the
function logs: aws logs tail /aws/lambda/MyFunction --follow.
Note
SAM CLI also supports invoking deployed functions with sam remote invoke and resolving
callbacks with sam remote callback succeed. See SAM CLI.
See also¶
- Authoring Set up the local test runner.
- Assertions Inspect operation history after a test run.