TypeScript에서 AWS Lambda Durable Functions 테스트

발행: (2025년 12월 3일 오전 08:46 GMT+9)
6 min read
원문: Dev.to

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()는 모든 것을 정리합니다.

beforeAllafterAll에서 테스트 파일당 한 번씩 호출합니다. 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
Back to Blog

관련 글

더 보기 »

🧱 강의 9B : 제품 관리 (Angular)

소개 이 모듈은 전체 제품 관리 기능을 구축하는 데 중점을 두며, 전체 CRUD 작업과 프론트엔드와의 원활한 통합을 포함합니다.