Simulated AWS SDK
Yulin can intercept AWS SDK clients and route their Commands to simulated AWS services. The code under test uses the AWS SDK exactly as it would in production, and never needs to know that it’s dealing with a simulator behind the scenes.
This is the recommended way to use Yulin for testing implementation code that already uses the AWS
SDK. Direct interaction with SimAws remains useful for seeding and inspecting simulated state
from within tests.
How it works
Section titled “How it works”SimSdk replaces the send method of an intercepted SDK client. Each sent Command is routed by
name to the matching operation of a simulated AWS service, and the result is returned to the
caller as a normal SDK response. Nothing touches the network.
Every SimSdk owns a simulated AWS environment. You can let it create its own, or give it an
existing one to share:
new SimSdk()creates an isolatedSimAwsinternally, available assimSdk.simAws.new SimSdk({ simAws })wraps aSimAwsyou already have.
Basic usage
Section titled “Basic usage”Intercept an SDK client class, then use the SDK as normal:
/** * Intercepting the S3 SDK client with simulated AWS behind it. */
import { CreateBucketCommand, GetObjectCommand, PutObjectCommand, S3Client,} from "@aws-sdk/client-s3";import { SimSdk } from "@kensio/yulin/sdk";
const simSdk = new SimSdk();simSdk.intercept(S3Client); // Intercepts every instance of the class.
// From here on, this is ordinary AWS SDK code.const s3Client = new S3Client({ region: "eu-west-2" });await s3Client.send(new CreateBucketCommand({ Bucket: "foo-bucket" }));await s3Client.send( new PutObjectCommand({ Bucket: "foo-bucket", Key: "hello.txt", Body: "Hello, world!", }),);
const output = await s3Client.send( new GetObjectCommand({ Bucket: "foo-bucket", Key: "hello.txt" }),);console.log(await output.Body?.transformToString()); // "Hello, world!"
simSdk.restoreAll();You can intercept a client class or a client instance:
- A class (
simSdk.intercept(S3Client)) intercepts every instance of it, including instances the code under test constructs later. This is the most common choice. - An instance (
simSdk.intercept(s3Client)) intercepts only that instance, which is useful when a specific client should hit the simulator while others are handled differently.
A client can only have one interception at a time: intercepting an already-intercepted client throws a diagnostic error rather than silently replacing the existing interception.
Account and Region scope
Section titled “Account and Region scope”The simulated Account and Region scope is resolved for each sent Command, never fixed per client:
- The Region comes from the sending client’s own configuration, such as
new S3Client({ region: "eu-west-2" }), falling back to the simulation default. - The Account comes from the ambient
simAws.runAs(...)caller when one is set, falling back to the simulation default Account.
The resolved caller is passed through to the simulated service, so simulated IAM authorization applies to it exactly as for direct sim service use: a caller without permission for a Command is denied, like real AWS. When no caller can be identified, Commands run as the simulation’s default Account root.
runAs runs a function with an ambient simulated caller, such as an IAM Role. Commands sent
during the run are attributed to that caller — with no changes to the client or the code under
test:
/** * Attributing intercepted SDK Commands to a caller with runAs. */
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";import { CreateBucketCommand, ListBucketsCommand, S3Client,} from "@aws-sdk/client-s3";import { SimAws } from "@kensio/yulin";import { SimSdk } from "@kensio/yulin/sdk";
const simAws = new SimAws();const simSdk = new SimSdk({ simAws });
// Seed a Bucket, and a Role allowed to list Buckets, in a simulated Account.const account = simAws.account("222222222222");await account .s3() .createBucket(new CreateBucketCommand({ Bucket: "team-bucket" }));await account.iam().createRole( new CreateRoleCommand({ RoleName: "TeamRole", AssumeRolePolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Principal: { AWS: "arn:aws:iam::222222222222:root" }, Action: "sts:AssumeRole", }, }), }),);await account.iam().putRolePolicy( new PutRolePolicyCommand({ RoleName: "TeamRole", PolicyName: "list-buckets", PolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Action: "s3:ListAllMyBuckets", Resource: "*", }, }), }),);
const s3Client = new S3Client({ region: "us-east-1" });simSdk.intercept(s3Client);
await simAws.runAs( { kind: "arn", arn: "arn:aws:iam::222222222222:role/TeamRole" }, async () => { // Sent as the TeamRole caller: resolved in Account 222222222222 and // authorized against the Role's simulated IAM permissions. const output = await s3Client.send(new ListBucketsCommand({})); console.log(output.Buckets); // [{ Name: "team-bucket" }] },);
simSdk.restoreAll();The ambient caller is scoped to its own SimAws instance, so separate simulations in the same
process never observe each other’s callers.
Restoring interception
Section titled “Restoring interception”Restoring puts back the client’s real SDK send:
interception.restore()restores one interception;simSdk.intercept(...)returns the handle.simSdk.restoreAll()restores everything intercepted through thatSimSdk.SimSdkand interception handles are disposable, sousing simSdk = new SimSdk();restores automatically at the end of the scope — convenient in tests.
Choosing Commands to intercept
Section titled “Choosing Commands to intercept”By default every Command sent through an intercepted client is routed to the simulator. To
intercept only specific Commands, pass an allow list of Command classes or names:
simSdk.intercept(s3Client, { commands: [GetObjectCommand] }). Commands outside the allow list
throw a diagnostic error.
Supported services and Commands
Section titled “Supported services and Commands”All simulated services support SDK interception: ACM, CloudFormation, CloudFront, DynamoDB, IAM, Route53, S3, and STS. Each service’s own docs under docs/services list the Commands it simulates.
Sending a Command the simulated service doesn’t support throws an error naming the Command and listing the supported ones, so gaps show up clearly in test output rather than as silent misbehaviour. Clients for AWS services Yulin doesn’t simulate at all are rejected the same way.
Limitations
Section titled “Limitations”- Only
client.send(command)is intercepted. SDK utilities that bypasssend, such asgetSignedUrl, are not simulated. Paginators and waiters go throughsend, so they work. - Simulated errors carry SDK-shaped
nameand$metadata, but are not instances of the real SDK exception classes: match errors witherror.name, notinstanceof. - The callback form of
send(command, callback)is not supported; use the promise form.
