AppSync has three types of GQL operations: Queries, Mutations, and Subscriptions. These operations can be divided into two broad categories: Synchronous and Asynchronous.
Queries and Mutations are straightforward to implement and test due to their synchronous nature. Subscriptions are more complicated, both in how they are put together and how they are validated. Today, I want to focus on that validation: how to test AppSync subscriptions.
Subscription Complexity
What makes GQL subscriptions so complex? First, they are tied to a mutation. This mutation is often implemented as an internal-only, "dummy"1 mutation to guarantee the projected properties. See Tamas Sallai's excellent article on why this is so, linked below in References.
Now, we want to test this complex thing we just built. Subscriptions work off of asynchronous events. I have previously written2 about whether you should test your application's eventing. Sometimes, it's not worth the effort. However, with GQL subscriptions, I have much more to test than a mere call to EventBridge. There is too much that can go wrong to forego these tests.
The last thing that makes testing GQL subscriptions difficult is the tooling. I experienced issues combining multiple subscriptions per Jest test fixture. Plus, the teardown can take many seconds for each fixture as the connections clean themselves upon termination. I expect this to improve over time as the tooling and techniques improve.
Our Example
Let's assume that our user, Jack, uses a popular e-commerce site and wants to be alerted if one of his favorite stores, Acme, adds a new product. Here is a diagram of the basic flow.
- The products service emits a "New Product Added to Acme" event.
- This event triggers a Lambda in our AppSync project, listening for new product events.
- This Lambda invokes a "dummy" mutation,
notifyProductAdded
- The dummy mutation triggers the subscription
- Jack receives his notification
There is a lot that can go wrong. We need tests.
Choosing Your Testing Width
Let's examine two subscription testing approaches: "wide" and "narrow."
The wide test involves both the resource service and AppSync. In this case, we call the resource service with an operation, "Create a Product," and let the event flow through AppSync.
The narrow approach stays within the AppSync application and begins by directly invoking the event-handling Lambda. In both cases, we spin up a GQL subscription before starting the test and validating the response.
Let's examine some of the techniques for setting up tests like this. Later, we'll consider why you might choose "wide" over "narrow" or vice versa.
Subscription Testing Techniques
I will be testing in NodeJS using the Jest framework. Many GQL libraries in NPM have great support for subscriptions. However, almost all are intended to work in a Web browser, not NodeJS. I had some success using the aws-amplify
package. I handle the entire subscription setup using one method: setUpSubscription
. Let's look at the key sections of this method.
Before you go any further, you must import and require WS, a Node.js WebSocket library. Amplify is also intended for a browser and expects to use the browser-native WebSocket object. To test in NodeJS, you must include this line.
The first thing I do is to configure Amplify for my application. In this case, I am using an API key to authorize the call to subscribe.
The next section spins up a Hub listener, which emits events about the connection itself. I use this to ensure I am fully connected before triggering the dummy mutation. The setup returns a function I call stopHubListener
to cleanly shut down the Hub after the test completes.
Next, I set up the subscription itself. This portion is straightforward and looks like any GQL invocation. Note that setting up the next()
handler will add a result to the array the caller passed in, validating that a message was received through the subscription.
Finally, I wait until my Hub listener is ready. When it is safe to exercise the subscription, it will set its connection state to "Connected."
The examples I used above are in a working application you can review at this GitHub repository. Please see the source code for more details than I provided today.
From here, the test is easy. Depending on your starting point, wide or narrow, you invoke the resource service or the Lambda, respectively. Then, you wait for the expected message to appear in your results array.
A Note of Caution
I had difficulties combining multiple subscription tests into the same test fixture. I don't know whether this was a Jest/Amplify limitation or my limited knowledge. Breaking them into many fixtures (files in Jest) works fine for me, as I run fixtures in parallel with my test runners.
When you have problems, especially lingering WebSocket connections, your tests will hang after passing or failing. These issues are difficult to unwind. Go slow. Build things in small chunks, and keep your test suite healthy as you go.
Other Auth Mechanisms
The example above uses an API key as its primary authorization. What if you don't use an API key? The Amplify library has you covered. It supports all the auth schemes that AppSync does, including custom (Lambda) authorization.
To support custom/Lambda authorization for your subscription, you must make two small changes to your subscription setup. The first is to simplify the call to Amplify.configure()
and only pass in the endpoint and region.
The other change is to the call to subscribe. Here, you pass in the authorization mode and token.
The remainder of the method should look identical to our API key version.
Wide or Narrow?
Where should your tests begin, at the resource service (wide) or with your event handler (narrow)? There are pros and cons to each approach.
A wide test tells you the most information. However, it couples your test to the resource service. If that service is experiencing problems, a wide test will fail and block your deployment pipeline.
A narrow test can be implemented ahead of time and independently from the resource service. It provides information about your AppSync API but not its interaction with other resource services.
The fundamental guideline is that your testing width should follow your deployment topology. Is the AppSync API part of the resource service’s deployment? Use wide tests. Is it an independently deployed unit? Use narrow tests. Keeping your deployment pipelines independent is crucial to managing a microservices architecture. Watch out for unnecessary coupling between projects.
Summary
AppSync subscriptions can be complicated. In the projects I've built, they were the most complex endpoints to set up. Knowing they work as expected goes a long way to building confidence that your AppSync API is solid. I hope this post helps you build that confidence.
Happy building!
References
Ownership Matters: Testing EventBridge with Serverless
GitHub: Serverless Reference Architectures appsync-to-http
Tamas Sallai: Real-time data with AppSync subscriptions
Ownership Matters: Testing AppSync JavaScript Resolvers
-
I first heard Yan Cui use this term, but I don't know if it originated with him. ↩
Top comments (0)