Test Event-Driven Architectures with EventBridge and AppSync Events
Testing Event-Driven applications is difficult. Last year, I wrote a piece on testing that your application(s) produce the events you expect, using Momento Topics. At the time, the setup was a lot simpler than what was possible with AppSync.
AWS introduced AppSync Events a couple of weeks back. When reading the announcement, I immediately thought of my old blog post. I hoped that AppSync Events could be used to build a solution that’s as simple as the previous one, using only AWS services.
In this post, I will show you how to set up the required resources to test your event-producing application(s) end-to-end.
TLDR; Show me the code!
You can find the sample application and a step-by-step guide on GitHub.
AppSync Events
In the release post, AWS describes AppSync Events as follows:
Today, AWS AppSync announced AWS AppSync Events, a feature that lets developers easily broadcast real-time event data to a few or millions of subscribers using secure and performant serverless WebSocket APIs. With AWS AppSync Events, developers no longer have to worry about building WebSocket infrastructure, managing connection state, and implementing fan-out. Developers simply create their API, and publish events that are broadcast to clients subscribed over a WebSocket connection. AWS AppSync Event APIs are serverless, so you can get started quickly, your APIs automatically scale, and you only pay for what you use.
Compared to earlier alternatives, such as AppSync GraphQL subscriptions and API Gateway WebSocket API, AppSync Events are much simpler to set up. You only have to create an API, define configuration such as default authorization, and create a channel namespace. Then, you can start publishing and subscribing to channels in the namespace.
We will only use the basic features of AppSync Events in this post. We will create a single namespace and protect it using API_KEY
authorization.
For the curious, there are more advanced features available, such as per-namespace authorization and custom handlers for onPublish
and onSubscribe
Sample Application
The sample application is very similar to the application in the previous post. It’s a simple Order Service, consisting of an API Gateway with a single POST /event
endpoint that triggers a Lambda function. This function generates a new order item and publishes an OrderCreated
event to EventBridge.
To recap, the Lambda function looks like this:
When a POST /event
request is sent to the API Gateway, this handler is invoked, publishing an event with the following structure to EventBridge:
The test setup will also be very similar to before. But let’s hold off on and start with the necessary test resources.
Test infrastructure
Last time, I used another SAM template to separate the application infrastructure from the test infrastructure. To add some variation, I’m using CDK this time. I decided to create a custom construct called TestResources
. I also added a stack property to conditionally deploy the resources in the environments where it makes sense:
So what test resources do we need? Let’s start with the obvious one, an AppSync Events API.
AppSync Events API
The core component of the test infrastructure is the AppSync Events API. For this demo, I will create an API with a namespace called default
and use an API key for authorization. At the time of writing, there are still no L2 constructs for AppSync Events, so we’ll have to do with L1 constructs for now.
The above creates an AppSync Events API with API_KEY
authorization. I’m setting the API key’s expiration to a year from now during each deployment for demo purposes. In a real-world scenario, you’d want to manage the API key’s lifecycle more carefully.
Attempt #1: EventBridge API Destination
The release post included a section on how to integrate EventBridge and AppSync Events using API destinations. In the example, an event with the following detail is used:
In my case, I want to send arbitrary events (such as OrderCreated
) to a specific channel. With Momento, this was as straightforward as setting the topic name as part of the URL in the API destination and sending the event payload in the body.
The request format in AppSync Events is a bit different. AppSync expects the payload to contain both the destination channel and up to five events. Each event must be stringified JSON, like {\"message\":\"Hello from EventBridge!\"}
above. Sadly, this request schema makes it impossible to directly integrate EventBridge and AppSync Events without having something in between.
For my first attempt, I’d assume that I could use a template like this:
However, this produces the following output:
I need the event to be stringified, but there’s no way to do that with EventBridge input transformation. That AWS chose a request schema for AppSync that cannot be used this way with API destinations is a blunder, to be frank.
The integration only works if the EventBridge publisher is aware of the AppSync namespace and channel and can format the event accordingly. At that point, the publisher might as well publish to the AppSync Events API directly.
We require something between EventBridge and AppSync, but what?
Attempt #2: Lambda Function
After wrangling with API destinations for far too long, I took the easy way out. I wired an EventBridge rule to a basic Lambda function:
This works and since the function is only used for testing purposes, cold starts aren’t a problem. But, it feels wrong to have to do this for such a simple transformation. Let’s try another way.
Attempt #3: Express Step Function
The only thing we need is to transform the input and send it to the AppSync Events API. This sounds like the perfect job for a Step Function. I will create an express state machine to transform the payload with JSONata and send it to AppSync using an HTTP task.
Let’s extend our TestResources
construct. First, create a new EventBridge connection that the HTTP task can use:
Then, add the state machine. In this case, there is only one step, so I’ll inline the state machine definition. I’d recommend keeping it in a separate .yml
file for more complex state machines.
A lot is happening here. Let’s walk through it.
I’m setting the workflow type to express. Express workflows are ideal for short-running jobs such as this. I’m creating a log group and configuring logging on the state machine to be able to view executions in the console.
The state machine comprises a single HTTP task that uses the EventBridge connection created earlier to send requests to AppSync. The state input is transformed into the format AppSync expects with a little help from the JSONata $string
function.
Next, I add a couple of permissions to the state machine:
Finally, I glue everything together by creating an EventBridge rule that forwards events from the event bus to the state machine:
For demo purposes, I’m forwarding all events. If you want to use this method to run tests against production, you most likely only want test events to flow through the test infrastructure.
If you can distinguish test from production traffic in your event-producing services, you can format your event to allow filtering out only test events.
Verify the test infrastructure
Let’s verify that the test infrastructure is wired correctly before moving on. The AppSync console for Event APIs has a neat Pub/Sub Editor that lets you connect and subscribe to the API directly in the browser.
Connect to your API and subscribe to the debug
channel in the default
namespace. Then, go to the console for your event bus and send an event or two. The events you send should show up in the AppSync console.
Test the sample application end-to-end
With the test infrastructure in place, we can now test our application End-to-End and be confident that it emits the correct event(s). The full architecture looks like this:
The test code is very similar to the test code in my previous post. The major difference is that it subscribes to AppSync instead of Momento.
The test case itself is almost identical:
First, we subscribe
to the debug
channel in the default
namespace. The test then creates an order through the API and stores the returned order ID. Then, it waits a maximum of five seconds for a message matching the expected format to appear on the channel.
The major difference from the Momento post is how subscribe
works:
I’m using the AWS Amplify client to handle the connection for me. The Amplify implementation requires a WebSocket client to be available. In node, this was previously available behind an experimental flag. Starting with Node.js 22, a browser-compatible implementation of WebSocket
is enabled by default. For earlier versions, you can use the ws package and set globalThis.WebSocket = WebSocket
.
In the subscribe
function, I create an RxJS ReplaySubject and connect and subscribe to the AppSync Events API using the Amplify client. All incoming messages are stored in the ReplaySubject
.
Individual tests can use the waitForMessageMatching
to find events matching a specific pattern. In our test, we are looking for an OrderCreated
event:
Conclusion
Serverless applications are inherently event-driven and can be challenging to test. Developers have gone to great lengths to try and emulate managed AWS services locally to facilitate testing. I’m a strong advocate for testing in the cloud, as it is the only way to truly test the behavior of your application(s) in a production-like environment.
With the introduction of AppSync Events, AWS has made it easier to set up pub/sub patterns using websockets with little to no effort. In this post, you have learned how to combine EventBridge, Step Functions, and AppSync Events to test that your application(s) emit the correct events.
Try it out!
You can find the sample application and a step-by-step guide on GitHub.