AWS service testing for Node.js. When a session starts, AWS_ENDPOINT_URL_* environment variables are set automatically — every AWS SDK v3 client in your application talks to local services without any code changes.
Install, create a session in beforeAll, and write a test. Local services start as a subprocess — your application's AWS SDK clients redirect to them automatically.
npm install local-web-services-javascript-sdk
# also requires local-web-services (Python) for ldk dev
pip install local-web-services
// jest.config.js
module.exports = {
testEnvironment: 'node',
testTimeout: 60000, // allow time for ldk dev to start
};
// orders.test.js
const { LwsSession } = require('local-web-services-javascript-sdk');
let session;
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 — it uses standard AWS SDK clients
const { createOrder } = require('./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 } = require('./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.
When a session starts, AWS_ENDPOINT_URL_* environment variables are set for every supported service. Any new DynamoDBClient({}) your application creates will automatically use local services — no client reconfiguration, no dependency injection.
// orders.js — unchanged
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs');
// Standard clients — no endpoint config
const dynamo = new DynamoDBClient({});
const sqs = new SQSClient({});
async function createOrder(order) {
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),
}));
}
module.exports = { createOrder };
// orders.test.js
const { LwsSession } = require('local-web-services-javascript-sdk');
let session;
beforeAll(async () => {
session = await LwsSession.create({
tables: [{ name: 'Orders', partitionKey: 'id' }],
queues: ['OrderQueue'],
});
// AWS_ENDPOINT_URL_DYNAMODB and AWS_ENDPOINT_URL_SQS
// are now set — the clients in orders.js pick them
// up automatically on their next call.
});
test('creates an order', async () => {
const { createOrder } = require('./orders');
await createOrder({ id: '42', status: 'pending' });
await session.dynamodb('Orders')
.assertItemExists({ id: { S: '42' } });
await session.sqs('OrderQueue')
.assertMessageCount(1);
});
Sets AWS_ENDPOINT_URL_DYNAMODB, AWS_ENDPOINT_URL_SQS, AWS_ENDPOINT_URL_S3, and more. Restored to original values when session.close() is called.
Application code is identical to production. new DynamoDBClient({}) without any endpoint override just works — the AWS SDK v3 reads the env var automatically.
Local services accept any credentials. No AWS account, no IAM setup, no ~/.aws/credentials needed in your test environment.
All factory methods are async — they start ldk dev and wait until services are ready before resolving.
const { LwsSession } = require('local-web-services-javascript-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();
const dynamo = session.client('dynamodb');
const sqs = session.client('sqs');
const s3 = session.client('s3');
const sns = session.client('sns');
const ssm = session.client('ssm');
const secrets = session.client('secretsmanager');
const sfn = session.client('stepfunctions');
const queueUrl = session.queueUrl('OrderQueue'); // local SQS URL
const port = session.portFor('dynamodb'); // port number
await session.reset(); // clear all tables, queues, buckets
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,
});
// Clear all fakes for a service
await session.fake('dynamodb').clear();
Inject failure conditions programmatically to 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();
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();