AWS service testing for TypeScript and Node.js. Use your existing AWS SDK v3 code unchanged — the SDK provides pre-configured clients that point to local services instead of AWS. No credentials, no deploy, no waiting.
Install, create a session in beforeAll, and write a test. Local services start as a subprocess — your AWS SDK v3 clients talk to them automatically.
npm install local-web-services-typescript-sdk
# also requires local-web-services (Python) for ldk dev
pip install local-web-services
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testTimeout: 60000, // allow time for ldk dev to start
};
// orders.test.ts
import { LwsSession } from 'local-web-services-typescript-sdk';
let session: LwsSession;
beforeAll(async () => {
session = await LwsSession.create({
tables: [{ name: 'Orders', partitionKey: 'id' }],
queues: ['OrderQueue'],
});
});
afterAll(async () => {
await session.close();
});
beforeEach(async () => {
await session.reset(); // wipe state between tests
});
test('creates an order', async () => {
// Call your real application code
const { createOrder } = await import('./orders');
await createOrder({ id: '42', status: 'pending' });
// Assert using the DynamoDB helper
const table = session.dynamodb('Orders');
const item = await table.assertItemExists({ id: { S: '42' } });
expect(item.status.S).toBe('pending');
});
test('order sends a queue message', async () => {
const { createOrder } = await import('./orders');
await createOrder({ id: '99', status: 'pending' });
const queue = session.sqs('OrderQueue');
await queue.assertMessageCount(1);
});
npx jest
The session starts ldk dev once in beforeAll, then reset() wipes state before each test.
Your application already uses AWS SDK v3. The SDK returns fully-typed DynamoDBClient, SQSClient, and S3Client instances pre-configured to talk to local services. Nothing in your application needs to change.
// orders.ts — unchanged
import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb';
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
const dynamo = new DynamoDBClient({});
const sqs = new SQSClient({});
export async function createOrder(order: { id: string; status: string }) {
await dynamo.send(new PutItemCommand({
TableName: 'Orders',
Item: {
id: { S: order.id },
status: { S: order.status },
},
}));
await sqs.send(new SendMessageCommand({
QueueUrl: process.env.ORDER_QUEUE_URL!,
MessageBody: JSON.stringify(order),
}));
}
// orders.test.ts
import { LwsSession } from 'local-web-services-typescript-sdk';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
let session: LwsSession;
beforeAll(async () => {
session = await LwsSession.create({
tables: [{ name: 'Orders', partitionKey: 'id' }],
queues: ['OrderQueue'],
});
// Pre-configured client — same type as your app uses
const client = session.client<DynamoDBClient>('dynamodb');
});
test('creates an order', async () => {
const { createOrder } = await import('./orders');
await createOrder({ id: '42', status: 'pending' });
const table = session.dynamodb('Orders');
await table.assertItemExists({ id: { S: '42' } });
});
Local services accept any credentials. No AWS account, no IAM setup, no environment variables required in your test environment.
Application code is identical to production. The SDK starts local services and configures the endpoint — no dependency injection, no mocking libraries.
session.client<DynamoDBClient>('dynamodb') returns the exact same type your application uses. Full TypeScript inference and IDE completion.
All factory methods are async — they start ldk dev and wait until services are ready before resolving.
import { LwsSession } from 'local-web-services-typescript-sdk';
// Explicit resource declaration
const session = await LwsSession.create({
tables: [
{ name: 'Orders', partitionKey: 'id' },
{ name: 'Products', partitionKey: 'sku', sortKey: 'version' },
],
queues: ['OrderQueue', 'DeadLetterQueue'],
buckets: ['ReceiptsBucket'],
});
// Auto-discover from CDK cloud assembly (cdk.out/)
const session = await LwsSession.fromCdk('../my-cdk-project');
// Auto-discover from Terraform .tf files
const session = await LwsSession.fromHcl('../my-terraform-project');
// Always close after your tests
await session.close();
import {
DynamoDBClient,
SQSClient,
S3Client,
SNSClient,
SSMClient,
SecretsManagerClient,
SFNClient,
} from '@aws-sdk/client-*';
const dynamo = session.client<DynamoDBClient>('dynamodb');
const sqs = session.client<SQSClient>('sqs');
const s3 = session.client<S3Client>('s3');
const sns = session.client<SNSClient>('sns');
const ssm = session.client<SSMClient>('ssm');
const secrets = session.client<SecretsManagerClient>('secretsmanager');
const sfn = session.client<SFNClient>('stepfunctions');
await session.reset(); // clear all tables, queues, buckets
Typed async helpers with built-in assertion methods. Skip the boilerplate.
const table = session.dynamodb('Orders');
await table.put({ id: { S: '1' }, status: { S: 'pending' } });
const item = await table.get({ id: { S: '1' } });
await table.delete({ id: { S: '1' } });
const items = await table.scan();
await table.assertItemExists({ id: { S: '1' } }); // returns item
await table.assertItemCount(5);
const queue = session.sqs('OrderQueue');
await queue.send({ orderId: '42', total: 99.99 }); // object → JSON
await queue.send('plain text message');
const messages = await queue.receive({ maxMessages: 10 });
await queue.purge();
await queue.assertMessageCount(3);
console.log(queue.url); // http://localhost:.../OrderQueue
const bucket = session.s3('ReceiptsBucket');
await bucket.put('receipts/001.pdf', Buffer.from('PDF bytes'));
await bucket.put('config.json', JSON.stringify({ key: 'value' }));
const data = await bucket.get('receipts/001.pdf'); // Buffer
const text = await bucket.getText('config.json'); // string
const keys = await bucket.listKeys({ prefix: 'receipts/' });
await bucket.delete('config.json');
await bucket.assertObjectExists('receipts/001.pdf');
await bucket.assertObjectCount({ expected: 5, prefix: 'receipts/' });
Override any AWS operation response with a fluent builder. No changes to application code needed.
// Inject throttling
await session.fake('dynamodb').operation('PutItem').respond({
status: 400,
body: { __type: 'ProvisionedThroughputExceededException' },
});
// Add artificial latency
await session.fake('sqs').operation('SendMessage').respond({
status: 200,
delayMs: 500,
});
// Match on a specific request header
await session.fake('s3')
.operation('GetObject')
.withHeader('x-request-id', 'test-123')
.respond({ status: 403, body: { __type: 'AccessDenied' } });
// Clear all fakes for a service
await session.fake('dynamodb').clear();
Inject failure conditions programmatically and verify your application handles them gracefully.
await session.chaos('dynamodb').errorRate(0.3).latency({ minMs: 50, maxMs: 200 }).apply();
await session.chaos('sqs').connectionResetRate(0.1).timeoutRate(0.05).apply();
await session.chaos('dynamodb').clear();
test('handles DynamoDB latency', async () => {
await session.chaos('dynamodb').latency({ minMs: 100, maxMs: 500 }).apply();
const start = Date.now();
const { createOrder } = await import('./orders');
await createOrder({ id: '42', status: 'pending' });
const elapsed = Date.now() - start;
expect(elapsed).toBeGreaterThanOrEqual(100);
await session.chaos('dynamodb').clear();
});
Switch IAM modes and define named identities with inline policies to test authorization behaviour.
await session.iam.mode('enforce').apply();
await session.iam.mode('audit').apply();
await session.iam.mode('disabled').apply(); // default
await session.iam.defaultIdentity('service-account').apply();
await session.iam
.identity('readonly')
.allow(['dynamodb:GetItem', 'dynamodb:Scan'])
.apply();