TypeScript에서 AWS Lambda Durable Functions 테스트
Source: Dev.to
테스트 라이브러리
SDK는 다양한 시나리오에 대한 테스트 도구를 제공합니다:
- LocalDurableTestRunner는 시뮬레이션된 체크포인트 서버와 함께 함수 를 프로세스 내에서 실행합니다. 테스트는 밀리초 단위로 실행되며, 일반적으로 몇 시간이 걸리는 워크플로도 마찬가지입니다. 대부분의 테스트에 사용할 것입니다.
- CloudDurableTestRunner는 AWS에 배포된 Lambda 함수에 대해 테스트합니다. 통합 테스트나 실제 AWS 환경에서 동작을 검증해야 할 때 사용합니다.
- run-durable CLI는 테스트 코드를 작성하지 않고도 빠르게 명령줄 테스트를 할 수 있게 해줍니다. 개발 중 빠른 반복 및 디버깅에 적합합니다.
우리는 대부분의 시간을 할애하게 될 로컬 테스트에 집중할 것입니다.
첫 번째 테스트
간단한 durable 함수부터 시작해 보겠습니다:
// order-processor.ts
import { DurableContext, withDurableExecution } from '@aws/durable-execution-sdk-js';
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const order = await context.step('create-order', async () => {
return { orderId: '123', total: 50 };
});
await context.wait({ seconds: 300 }); // Wait 5 minutes
const notification = await context.step('send-notification', async () => {
return { sent: true };
});
return { order, notification };
}
);
다음은 이를 테스트하는 방법입니다:
// order-processor.test.ts
import { LocalDurableTestRunner } from '@aws/durable-execution-sdk-js-testing';
import { handler } from './order-processor';
describe('Order Processor', () => {
beforeAll(async () => {
await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true });
});
afterAll(async () => {
await LocalDurableTestRunner.teardownTestEnvironment();
});
it('should process order successfully', async () => {
const runner = new LocalDurableTestRunner({
handlerFunction: handler,
});
const execution = await runner.run();
expect(execution.getStatus()).toBe('SUCCEEDED');
expect(execution.getResult()).toEqual({
order: { orderId: '123', total: 50 },
notification: { sent: true }
});
});
});
이게 전부입니다. 함수에 5분 대기 시간이 있더라도 테스트는 밀리초 안에 실행됩니다. skipTime: true 옵션은 테스트 러너에게 시간 기반 작업을 즉시 건너뛰도록 지시합니다.
설정 및 정리
모든 테스트 스위트는 설정과 정리가 필요합니다:
beforeAll(async () => {
await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true });
});
afterAll(async () => {
await LocalDurableTestRunner.teardownTestEnvironment();
});
setupTestEnvironment()는 로컬 체크포인트 서버를 시작하고, 선택적으로 시간 건너뛰기를 위한 가짜 타이머를 설치합니다.teardownTestEnvironment()는 모든 것을 정리합니다.
beforeAll와 afterAll에서 테스트 파일당 한 번씩 호출합니다. skipTime 옵션은 빠른 테스트에 필수적입니다. 이 옵션을 사용하면 context.wait(), setTimeout, 재시도 지연과 같은 작업이 즉시 완료됩니다. 사용하지 않으면 테스트가 지정된 시간만큼 실제로 대기하게 됩니다.
작업 검사
테스팅 라이브러리의 진정한 힘은 함수가 수행한 작업을 검사하는 데 있습니다:
it('should execute operations in correct order', async () => {
const runner = new LocalDurableTestRunner({
handlerFunction: handler,
});
const execution = await runner.run();
// Get all operations
const operations = execution.getOperations();
expect(operations).toHaveLength(3); // create-order, wait, send-notification
// Get specific operation by index
const createOrder = runner.getOperationByIndex(0);
expect(createOrder.getType()).toBe('STEP');
expect(createOrder.getStatus()).toBe('SUCCEEDED');
expect(createOrder.getStepDetails()?.result).toEqual({
orderId: '123',
total: 50
});
// Get wait operation
const waitOp = runner.getOperationByIndex(1);
expect(waitOp.getType()).toBe('WAIT');
expect(waitOp.getWaitDetails()?.waitSeconds).toBe(300);
// Get notification operation
const notification = runner.getOperationByIndex(2);
expect(notification.getStepDetails()?.result).toEqual({ sent: true });
});
작업은 인덱스, 이름 또는 ID로 접근할 수 있습니다. 각 작업은 유형, 상태 및 단계 결과나 대기 시간과 같은 유형별 세부 정보를 제공합니다.
실패 및 재시도 테스트
실제 워크플로는 실패합니다. 재시도 동작을 테스트해 보겠습니다:
// payment-processor.ts
import { DurableContext, withDurableExecution, createRetryStrategy } from '@aws/durable-execution-sdk-js';
export const handler = withDurableExecution(
async (event: any, context: DurableContext) => {
const payment = await context.step('process-payment', async () => {
const response = await fetch('https://api.payments.com/charge', {
method: 'POST',
body: JSON.stringify({ amount: event.amount })
});
if (!response.ok) throw new Error('Payment failed');
return response.json();
}, {
retryStrategy: createRetryStrategy({
maxAttempts: 3,
backoffRate: 2,
initialInterval: 1000
})
});
return payment;
}
);
모크(mock)를 사용해 테스트합니다:
// payment-processor.test.ts
import { LocalDurableTestRunner } from '@aws/durable-execution-sdk-js-testing';
import { handler } from './payment-processor';
// Store original fetch
const originalFetch = global.fetch;
describe('Payment Processor', () => {
beforeAll(async () => {
await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true });
});
afterAll(async () => {
await LocalDurableTestRunner.teardownTestEnvironment();
});
beforeEach(() => {
// Mock fetch for external APIs only
global.fetch = jest.fn((url: string | URL | Request, ...args) => {
const urlString = url.toString();
// Let checkpoint server calls through
if (urlString.includes('127.0.0.1') || urlString.includes('localhost')) {
return originalFetch(url as any, ...args);
}
// Mock external API calls
return Promise.reject(new Error('Unmocked fetch call'));
}) as any;
});
afterEach(() => {
global.fetch = originalFetch;
});
it('should succeed on first attempt', async () => {
(global.fetch as jest.Mock).mockImplementation((url: string | URL | Request, ...args) => {
const urlString = url.toString();
if (urlString.includes('127.0.0.1') || urlString.includes('localhost')) {
return originalFetch(url as any, ...args);
}
if (urlString.includes('api.payments.com')) {
return Promise.resolve({
ok: true,
json: async () => ({ transactionId: 'txn-123', status: 'success' })
});
}
return Promise.reject(new Error('Unmocked fetch'));
});
const runner = new LocalDurableTestRunner({
handlerFunction: handler,
});
const execution = await runner.run({ payload: { amount: 100 } });
expect(execution.getStatus()).toBe('SUCCEEDED');
expect(execution.getResult()).toEqual({
transactionId: 'txn-123',
status: 'success'
});
});
it('should retry on failure and eventually succeed', async () => {
let callCount = 0;
(global.fetch as jest.Mock).mockImplementation