Skip to content
~/sph.sh

Serverless Uygulamaları Test Etmek: Pratik Bir Strateji Rehberi

AWS Lambda, API Gateway, DynamoDB ve Step Functions için hızlı geri bildirim ve production güvenilirliği sağlayan kapsamlı bir test stratejisi oluşturmayı öğrenin.

Test Etme Zorluğu

Serverless uygulamalarla çalışmak bana sezgisel olmayan bir şey öğretti: ne kadar hızlı deploy edebilirsen, test stratejin o kadar kritik hale geliyor. Değişiklikleri dakikalar içinde production'a gönderebiliyorsan, bu değişikliklerin doğru olması gerekiyor.

Serverless testing'i özellikle zorlaştıran şeyler şunlar:

Yanlış güven problemi: Testler mock edilmiş AWS SDK çağrılarıyla geçiyor, sonra production IAM izinleri veya yanlış event yapıları yüzünden başarısız oluyor.

Yavaş feedback döngüsü: Basit bir Lambda değişikliğini test etmek için CloudFormation deployment'larını birkaç dakika beklemek.

LocalStack boşluğu: Testler LocalStack'e karşı geçiyor ama API farklılıkları yüzünden gerçek AWS'de başarısız oluyor.

Async karmaşıklığı: EventBridge ve Step Functions güvenilir şekilde test edilmesi zor asenkron davranışlar getiriyor.

Bu yazıda, hızlı feedback ile production güvenilirliğini dengeleyen pratik serverless test pattern'lerini paylaşıyorum.

Serverless Test Piramidi

Geleneksel test piramidi serverless için de geçerli, ancak ayarlanmış oranlar ve tekniklerle:

Unit testler (70%): Hızlı, AWS çağrısı yok, business logic'i izole şekilde test et. Her commit'te çalıştır.

Integration testler (20%): Gerçek veya local AWS servisleriyle servis entegrasyonlarını test et. Pull request'lerde çalıştır.

E2E testler (10%): Cloud environment'ta full workflow testing. Main branch deployment'larında çalıştır.

Pratikte işe yarayanlar:

Her Test Seviyesini Ne Zaman Kullanmalı

Unit testler şunları yakalar:

  • Business logic hataları
  • Input validation sorunları
  • Data transformation bug'ları
  • Error handling eksiklikleri

Integration testler şunları yakalar:

  • IAM permission sorunları
  • Servis konfigürasyon problemleri
  • Event yapı uyumsuzlukları
  • Timeout senaryoları

E2E testler şunları yakalar:

  • Multi-service orchestration sorunları
  • Cross-account routing problemleri
  • Production konfigürasyon sapmaları
  • Gerçek performans sorunları

Lambda Function'larını Unit Test Etme

Etkili unit testing'in anahtarı handler'ı business logic'ten ayırmak:

typescript
// Kötü: Her şey handler içindeexport const handler = async (event: APIGatewayProxyEvent) => {  // Business logic handler kodu ile karışmış  const body = JSON.parse(event.body || '{}');  const discount = body.amount > 100 ? 0.1 : 0;  const total = body.amount * (1 - discount);
  return {    statusCode: 200,    body: JSON.stringify({ total })  };};
// İyi: Ayrı sorumluluklarexport const calculateTotal = (amount: number): number => {  const discount = amount > 100 ? 0.1 : 0;  return amount * (1 - discount);};
export const handler = async (event: APIGatewayProxyEvent) => {  const body = JSON.parse(event.body || '{}');  const total = calculateTotal(body.amount);
  return {    statusCode: 200,    body: JSON.stringify({ total })  };};

Şimdi calculateTotal'i API Gateway event'lerini mock'lamadan test edebilirsin.

AWS SDK v3 ile Test Etme

DynamoDB'den okuyan bir Lambda'yı test eden pratik bir örnek:

typescript
// user-handler.tsimport { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';import { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
const client = new DynamoDBClient({});const ddb = DynamoDBDocumentClient.from(client);
export const getUser = async (userId: string) => {  const result = await ddb.send(new GetCommand({    TableName: process.env.TABLE_NAME,    Key: { PK: `USER#${userId}` }  }));
  if (!result.Item) {    throw new Error('User not found');  }
  return result.Item;};
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {  try {    const userId = event.pathParameters?.id;    if (!userId) {      return { statusCode: 400, body: JSON.stringify({ error: 'Missing user ID' }) };    }
    const user = await getUser(userId);
    return {      statusCode: 200,      body: JSON.stringify(user)    };  } catch (error) {    return {      statusCode: 404,      body: JSON.stringify({ error: (error as Error).message })    };  }};

Bunu aws-sdk-client-mock ile test et:

typescript
// user-handler.test.tsimport { mockClient } from 'aws-sdk-client-mock';import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb';import { APIGatewayProxyEvent } from 'aws-lambda';import { handler, getUser } from './user-handler';
const ddbMock = mockClient(DynamoDBDocumentClient);
beforeEach(() => {  ddbMock.reset();  process.env.TABLE_NAME = 'users';});
describe('getUser', () => {  it('kullanıcı bulunduğunda döndürür', async () => {    ddbMock.on(GetCommand).resolves({      Item: { PK: 'USER#123', name: 'Ahmet', email: '[email protected]' }    });
    const user = await getUser('123');
    expect(user).toEqual({      PK: 'USER#123',      name: 'Ahmet',      email: '[email protected]'    });  });
  it('kullanıcı bulunamadığında hata fırlatır', async () => {    ddbMock.on(GetCommand).resolves({ Item: undefined });
    await expect(getUser('999')).rejects.toThrow('User not found');  });
  it('DynamoDB\'yi doğru parametrelerle çağırır', async () => {    ddbMock.on(GetCommand).resolves({ Item: { PK: 'USER#123' } });
    await getUser('123');
    expect(ddbMock.calls()[0].args[0].input).toEqual({      TableName: 'users',      Key: { PK: 'USER#123' }    });  });});
describe('handler', () => {  it('userId eksik olduğunda 400 döndürür', async () => {    const event = createApiGatewayEvent({ pathParameters: null });
    const result = await handler(event);
    expect(result.statusCode).toBe(400);    expect(JSON.parse(result.body)).toEqual({ error: 'Missing user ID' });  });
  it('kullanıcı bulunduğunda döndürür', async () => {    ddbMock.on(GetCommand).resolves({      Item: { PK: 'USER#123', name: 'Ahmet' }    });    const event = createApiGatewayEvent({ pathParameters: { id: '123' } });
    const result = await handler(event);
    expect(result.statusCode).toBe(200);    expect(JSON.parse(result.body)).toEqual({ PK: 'USER#123', name: 'Ahmet' });  });});
// Test event'leri oluşturmak için yardımcı fonksiyonfunction createApiGatewayEvent(overrides?: Partial<APIGatewayProxyEvent>): APIGatewayProxyEvent {  return {    body: null,    headers: {},    multiValueHeaders: {},    httpMethod: 'GET',    isBase64Encoded: false,    path: '/users',    pathParameters: null,    queryStringParameters: null,    multiValueQueryStringParameters: null,    stageVariables: null,    requestContext: {      accountId: '123456789012',      apiId: 'test-api',      protocol: 'HTTP/1.1',      httpMethod: 'GET',      path: '/users',      stage: 'test',      requestId: 'test-request',      requestTimeEpoch: Date.now(),      resourceId: 'test-resource',      resourcePath: '/users',      identity: {        sourceIp: '127.0.0.1',        userAgent: 'test-agent',        accessKey: null,        accountId: null,        apiKey: null,        apiKeyId: null,        caller: null,        clientCert: null,        cognitoAuthenticationProvider: null,        cognitoAuthenticationType: null,        cognitoIdentityId: null,        cognitoIdentityPoolId: null,        principalOrgId: null,        user: null,        userArn: null      },      authorizer: null    },    resource: '/users',    ...overrides  } as APIGatewayProxyEvent;}

Buradaki önemli pattern'ler:

  1. Sadece external bağımlılıkları mock'la (DynamoDB), business logic'i değil
  2. Business function'ı (getUser) handler'dan ayrı test et
  3. Sadece return değerlerini değil, gerçek AWS SDK çağrı parametrelerini doğrula
  4. Test event oluşturma için helper function'lar yarat
  5. Test kirlenmesini önlemek için beforeEach'te mock'ları resetle

Integration Test Stratejileri

Unit testler hızlı feedback verir ama entegrasyon sorunlarını yakalayamaz. İşte integration testing'in kritik hale geldiği durumlar.

Gerçek DynamoDB ile Test Etme

Önemli operasyonlar için gerçek DynamoDB'ye karşı test et:

typescript
// user-repository.integration.test.tsimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';import { DynamoDBDocumentClient, PutCommand, GetCommand, QueryCommand, DeleteCommand } from '@aws-sdk/lib-dynamodb';import { randomUUID } from 'crypto';
const client = new DynamoDBClient({  region: process.env.AWS_REGION || 'us-east-1'});const ddb = DynamoDBDocumentClient.from(client);
// Paralel execution'a izin vermek için test çalıştırması başına unique table adı kullanconst TABLE_NAME = `users-test-${Date.now()}`;const testUserIds: string[] = [];
beforeAll(async () => {  // Gerçek setup'ta table'ı AWS CDK veya CloudFormation ile oluştur  // Burada table'ın var olduğunu varsayıyoruz});
afterEach(async () => {  // Test verilerini temizle  await Promise.all(    testUserIds.map(id =>      ddb.send(new DeleteCommand({        TableName: TABLE_NAME,        Key: { PK: `USER#${id}` }      }))    )  );  testUserIds.length = 0;});
describe('DynamoDB Integration Tests', () => {  it('kullanıcı oluşturur ve getirir', async () => {    const userId = randomUUID();    testUserIds.push(userId);
    // Kullanıcı oluştur    await ddb.send(new PutCommand({      TableName: TABLE_NAME,      Item: {        PK: `USER#${userId}`,        name: 'Ahmet Yılmaz',        email: '[email protected]',        createdAt: new Date().toISOString()      }    }));
    // Kullanıcıyı getir    const result = await ddb.send(new GetCommand({      TableName: TABLE_NAME,      Key: { PK: `USER#${userId}` }    }));
    expect(result.Item).toMatchObject({      PK: `USER#${userId}`,      name: 'Ahmet Yılmaz',      email: '[email protected]'    });  });
  it('email GSI ile kullanıcıları sorgular', async () => {    const userId = randomUUID();    testUserIds.push(userId);    const email = `test-${userId}@example.com`;
    await ddb.send(new PutCommand({      TableName: TABLE_NAME,      Item: {        PK: `USER#${userId}`,        email: email,        name: 'Test User'      }    }));
    // GSI kullanarak email ile sorgula    const result = await ddb.send(new QueryCommand({      TableName: TABLE_NAME,      IndexName: 'EmailIndex',      KeyConditionExpression: 'email = :email',      ExpressionAttributeValues: { ':email': email }    }));
    expect(result.Items).toHaveLength(1);    expect(result.Items?.[0].PK).toBe(`USER#${userId}`);  });});

Bu neden önemli: Bu test gerçek sorunları yakalar:

  • IAM izinleri (test role'ün izinleri yoksa, başarısız olur)
  • GSI konfigürasyon problemleri
  • Conditional write çakışmaları
  • DynamoDB API değişiklikleri

Trade-off'lar: Unit testlerden daha yavaş (2-5 saniye vs 10ms), ayda birkaç cent maliyet.

LocalStack vs Gerçek AWS: Hangisini Ne Zaman Kullanmalı

Note: LocalStack'in Step Functions ve EventBridge entegrasyonunda bilinen sorunları var. Bu servisleri içeren workflow'ları test etmek için gerçek AWS kullan veya davranış farklılıklarına hazır ol.

Kullandığım karar framework'ü:

LocalStack için:

  • Basit DynamoDB operasyonları (GET, PUT, QUERY)
  • Temel S3 operasyonları
  • SQS mesaj işleme
  • Geliştirme sırasında hızlı iterasyon

Gerçek AWS için:

  • IAM permission validation
  • Step Functions orchestration
  • EventBridge event routing
  • Karmaşık DynamoDB stream'ler
  • Pre-deployment validation

API Gateway Entegrasyonlarını Test Etme

API Gateway event'leri karmaşık yapılara sahip. Bunları düzgün test etmenin yolu:

typescript
// api-integration.test.tsimport { APIGatewayProxyEvent } from 'aws-lambda';import { handler } from './api-handler';
describe('API Gateway Integration', () => {  it('JSON body ile POST request\'i işler', async () => {    const event: APIGatewayProxyEvent = {      httpMethod: 'POST',      path: '/users',      headers: {        'content-type': 'application/json'      },      body: JSON.stringify({        name: 'Ahmet Yılmaz',        email: '[email protected]'      }),      // ... diğer gerekli alanlar    } as APIGatewayProxyEvent;
    const result = await handler(event);
    expect(result.statusCode).toBe(201);    expect(result.headers).toMatchObject({      'content-type': 'application/json',      'access-control-allow-origin': '*' // CORS    });  });
  it('CORS header\'larını doğrular', async () => {    const event = createApiGatewayEvent({      httpMethod: 'OPTIONS',      headers: {        'origin': 'https://example.com',        'access-control-request-method': 'POST'      }    });
    const result = await handler(event);
    expect(result.headers).toMatchObject({      'access-control-allow-origin': '*',      'access-control-allow-methods': 'GET,POST,PUT,DELETE',      'access-control-allow-headers': 'Content-Type,Authorization'    });  });
  it('geçersiz JSON için 400 döndürür', async () => {    const event = createApiGatewayEvent({      httpMethod: 'POST',      body: 'invalid json{'    });
    const result = await handler(event);
    expect(result.statusCode).toBe(400);    expect(JSON.parse(result.body)).toMatchObject({      error: 'Invalid JSON'    });  });});

AWS SAM Local ile Test Etme

Daha yüksek doğrulukta testing için SAM CLI kullan:

bash
# Local API Gateway başlatsam local start-api --port 3000
# Başka bir terminal'de curl ile test etcurl -X POST http://localhost:3000/users \  -H "Content-Type: application/json" \  -d '{"name":"Ahmet","email":"[email protected]"}'
# Veya function'ı doğrudan test event ile çağırsam local invoke UserFunction --event events/create-user.json

events/create-user.json:

json
{  "httpMethod": "POST",  "path": "/users",  "headers": {    "content-type": "application/json"  },  "body": "{\"name\":\"Ahmet Yılmaz\",\"email\":\"[email protected]\"}"}

Bu şu sorunları yakalar:

  • Lambda timeout konfigürasyonu
  • Memory limit problemleri
  • Environment variable konfigürasyonu
  • Cold start davranışı

Step Functions Test Etme

Step Functions birden fazla servisi orkestra eder. Bunları test etmenin yolu:

Note: AWS Step Functions Local şu anda AWS tarafından desteklenmiyor ve uyumluluk sorunları olabiliyor. Güvenilir test için gerçek AWS Step Functions'ı test state machine'leriyle kullan.

Seviye 1: State Machine Definition Validation

typescript
// state-machine.test.tsimport * as fs from 'fs';import * as path from 'path';
describe('State Machine Definition', () => {  it('geçerli JSON syntax\'ına sahip', () => {    const definitionPath = path.join(__dirname, 'state-machine.asl.json');    const definition = JSON.parse(fs.readFileSync(definitionPath, 'utf8'));
    expect(definition).toHaveProperty('StartAt');    expect(definition).toHaveProperty('States');  });
  it('her state için error handling var', () => {    const definition = JSON.parse(fs.readFileSync('state-machine.asl.json', 'utf8'));
    Object.entries(definition.States).forEach(([name, state]: [string, any]) => {      if (state.Type === 'Task') {        expect(state).toHaveProperty('Catch',          `State ${name} error handling içermeli`        );      }    });  });});

Seviye 2: Gerçek Execution'larla Integration Testing

typescript
// step-functions-integration.test.tsimport {  SFNClient,  StartExecutionCommand,  DescribeExecutionCommand} from '@aws-sdk/client-sfn';
const sfn = new SFNClient({ region: 'us-east-1' });const STATE_MACHINE_ARN = process.env.STATE_MACHINE_ARN;
describe('Order Processing State Machine', () => {  it('siparişi başarıyla işler', async () => {    const executionName = `test-${Date.now()}`;
    // Execution başlat    const { executionArn } = await sfn.send(new StartExecutionCommand({      stateMachineArn: STATE_MACHINE_ARN,      name: executionName,      input: JSON.stringify({        orderId: '12345',        items: [{ id: 'item1', quantity: 2 }]      })    }));
    // Tamamlanmasını bekle (timeout ile)    const result = await waitForExecution(executionArn!, 30000);
    expect(result.status).toBe('SUCCEEDED');
    const output = JSON.parse(result.output!);    expect(output).toMatchObject({      orderId: '12345',      status: 'COMPLETED'    });  });
  it('validation hatalarını işler', async () => {    const executionName = `test-error-${Date.now()}`;
    const { executionArn } = await sfn.send(new StartExecutionCommand({      stateMachineArn: STATE_MACHINE_ARN,      name: executionName,      input: JSON.stringify({        orderId: '', // Geçersiz        items: []      })    }));
    const result = await waitForExecution(executionArn!, 30000);
    expect(result.status).toBe('FAILED');  });});
async function waitForExecution(executionArn: string, timeoutMs: number) {  const startTime = Date.now();
  while (Date.now() - startTime < timeoutMs) {    const { status, output } = await sfn.send(new DescribeExecutionCommand({      executionArn    }));
    if (status === 'SUCCEEDED' || status === 'FAILED') {      return { status, output };    }
    await new Promise(resolve => setTimeout(resolve, 1000));  }
  throw new Error('Execution timeout');}

EventBridge Test Etme

EventBridge testing asenkron delivery nedeniyle zor. İşte güvenilir bir pattern:

typescript
// eventbridge-integration.test.tsimport { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';import { DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
const eventBridge = new EventBridgeClient({ region: 'us-east-1' });const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));
describe('EventBridge Event Processing', () => {  it('user.created event\'ini işler', async () => {    const testId = `test-${Date.now()}`;
    // Event yayınla    await eventBridge.send(new PutEventsCommand({      Entries: [{        Source: 'user-service',        DetailType: 'user.created',        Detail: JSON.stringify({          userId: testId,          email: '[email protected]',          testMarker: testId // Test event'lerini tanımlamak için        })      }]    }));
    // İşlenmeyi bekle (EventBridge + Lambda execution)    await waitForEventProcessing(testId, 10000);
    // DynamoDB'deki side effect'leri doğrula    const result = await ddb.send(new QueryCommand({      TableName: 'event-log',      KeyConditionExpression: 'testMarker = :marker',      ExpressionAttributeValues: { ':marker': testId }    }));
    expect(result.Items).toHaveLength(1);    expect(result.Items?.[0]).toMatchObject({      eventType: 'user.created',      processed: true    });  });});
async function waitForEventProcessing(testId: string, timeoutMs: number) {  const startTime = Date.now();
  while (Date.now() - startTime < timeoutMs) {    const result = await ddb.send(new QueryCommand({      TableName: 'event-log',      KeyConditionExpression: 'testMarker = :marker',      ExpressionAttributeValues: { ':marker': testId }    }));
    if (result.Items && result.Items.length > 0) {      return;    }
    await new Promise(resolve => setTimeout(resolve, 500));  }
  throw new Error('Event processing timeout');}

Önemli pattern: Unique bir test marker kullan ve async event'leri doğrudan yakalamaya çalışmak yerine side effect'leri poll et.

CI/CD Pipeline Entegrasyonu

Test piramidini implement eden bir GitHub Actions workflow'u:

yaml
# .github/workflows/test.ymlname: Serverless Tests
on:  push:    branches: [main, develop]  pull_request:    branches: [main]
jobs:  unit-tests:    name: Unit Tests    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: '20'          cache: 'npm'
      - name: Bağımlılıkları yükle        run: npm ci
      - name: Unit testleri çalıştır        run: npm run test:unit -- --coverage
      - name: Coverage yükle        uses: codecov/codecov-action@v3
    # Hızlı: 30-60 saniye, Maliyet: $0
  integration-tests:    name: Integration Tests (LocalStack)    runs-on: ubuntu-latest    needs: unit-tests    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: '20'          cache: 'npm'
      - name: LocalStack başlat        run: |          docker run -d \            -p 4566:4566 \            -e SERVICES=dynamodb,s3,sqs \            localstack/localstack:latest
          # LocalStack hazır olmasını bekle          timeout 60 bash -c 'until curl -s http://localhost:4566/_localstack/health | grep -q "\"dynamodb\": \"available\""; do sleep 1; done'
      - name: Bağımlılıkları yükle        run: npm ci
      - name: Integration testleri çalıştır        run: npm run test:integration        env:          AWS_ENDPOINT: http://localhost:4566
    # Orta: 2-5 dakika, Maliyet: $0
  e2e-tests:    name: E2E Tests (Gerçek AWS)    runs-on: ubuntu-latest    needs: integration-tests    if: github.ref == 'refs/heads/main'
    permissions:      id-token: write      contents: read
    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: '20'          cache: 'npm'
      - name: AWS credentials yapılandır        uses: aws-actions/configure-aws-credentials@v4        with:          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}          aws-region: us-east-1
      - name: Bağımlılıkları yükle        run: npm ci
      - name: Test stack deploy et        run: |          npx cdk deploy TestStack \            --require-approval never \            --outputs-file outputs.json
      - name: E2E testleri çalıştır        run: npm run test:e2e
      - name: Test stack'i yok et        if: always()        run: npx cdk destroy TestStack --force
    # Yavaş: 5-15 dakika, Maliyet: ~$0.50 per run

package.json test script'leri:

json
{  "scripts": {    "test:unit": "jest --testPathPattern=\\.test\\.ts$",    "test:integration": "jest --testPathPattern=\\.integration\\.test\\.ts$",    "test:e2e": "jest --testPathPattern=\\.e2e\\.test\\.ts$ --runInBand"  }}

Yaygın Tuzaklar ve Çözümler

Tuzak 1: Aşırı Mock'lama

Problem: AWS SDK çağrılarını mock'lamak ama IAM permission sorunlarını kaçırmak.

typescript
// KÖTÜ: Test geçer, production'da başarısız olurddbMock.on(PutCommand).resolves({});// Deploy → "AccessDenied: User is not authorized to perform: dynamodb:PutItem"
// İYİ: Gerçek DynamoDB ile integration test bunu yakalarconst result = await ddb.send(new PutCommand({...}));// IAM izinleri yanlışsa test başarısız olur

Çözüm: Kritik path'ler için gerçek AWS ile integration testler kullan.

Tuzak 2: Event Yapı Uyumsuzlukları

Problem: Test event'leri gerekli alanları içermiyor.

typescript
// KÖTÜ: Eksik test eventconst event = { body: JSON.stringify({ id: 1 }) };
// İYİ: @types/aws-lambda kullanimport { APIGatewayProxyEvent } from 'aws-lambda';const event: APIGatewayProxyEvent = {  body: JSON.stringify({ id: 1 }),  headers: {},  httpMethod: 'POST',  isBase64Encoded: false,  path: '/users',  pathParameters: null,  queryStringParameters: null,  multiValueHeaders: {},  multiValueQueryStringParameters: null,  stageVariables: null,  requestContext: {    accountId: '123456789012',    apiId: 'test',    // ... tam context  },  resource: '/users'};

Çözüm: Event builder function'lar yarat veya kaydedilmiş gerçek event'leri kullan.

Tuzak 3: Async Davranışı Göz Ardı Etme

Problem: İşlenmeyi beklemeden async EventBridge'i test etme.

typescript
// KÖTÜ: Event henüz işlenmediawait eventBridge.putEvents({ Entries: [event] });const result = await queryResults(); // Boş!
// İYİ: İşlenmeyi bekleawait eventBridge.putEvents({ Entries: [event] });await waitForEventProcessing(); // Poll et veya Step Functions kullanconst result = await queryResults(); // Veri var

Çözüm: Polling implement et veya event işlemeyi track etmek için Step Functions kullan.

Tuzak 4: Test Environment Kirlenmesi

Problem: Paralel testler birbirini etkiliyor.

typescript
// KÖTÜ: Paylaşılan resource'larconst TABLE_NAME = 'users-test'; // Paralel testlerde çakışma
// İYİ: Unique resource adlarıconst TABLE_NAME = `users-test-${Date.now()}-${Math.random()}`;
// Veya beforeEach/afterEach cleanup kullanafterEach(async () => {  await Promise.all(    testItems.map(id => ddb.delete({ Key: { id } }))  );});

Çözüm: Unique resource adları kullan ve cleanup hook'ları implement et.

Önemli Çıkarımlar

Pratikte işe yarayanlar:

1. Test piramidini takip et: Hızlı feedback için %70 unit test, güven için %20 integration test, production validation için %10 E2E test.

2. Business logic'i handler'lardan ayır: Bu unit testing'i kolaylaştırır ve hızlandırır. Business logic'ini detaylıca test et, sonra handler wiring için daha hafif testler yap.

3. Hızlı iterasyon için LocalStack, validation için gerçek AWS kullan: LocalStack geliştirme hızı için harika, ama deploy etmeden önce her zaman gerçek AWS'ye karşı validate et.

4. IAM izinlerini açıkça test et: En yaygın "testte çalışır, prod'da başarısız olur" sorunu IAM izinleridir. Gerçek AWS ile integration testler bunları yakalar.

5. Event builder utility'leri oluştur: Gerçekçi test event'leri oluşturmak için helper function'lar yarat. Eksiksizlik için @types/aws-lambda type'larını kullan.

6. Düzgün cleanup implement et: Test kirlenmesini önlemek için afterEach/afterAll hook'ları ve unique resource adları kullan. Bu aynı zamanda AWS maliyetlerini de azaltır.

7. Async testing'i düzgün yönet: EventBridge ve Step Functions asenkrondur. Event işlemeyi validate etmek için polling implement et veya Step Functions execution'ları kullan.

8. CI/CD pipeline maliyetlerini optimize et: Her commit'te unit testler, PR'larda integration testler, sadece main branch'te E2E testler çalıştır. Auto-deletion ile ephemeral stack'ler kullan.

9. Test metriklerini track et: Execution time, flakiness rate ve AWS maliyetlerini izle. Varsayımlara değil, veriye dayalı optimize et.

10. Basit başla: Temel unit testlerle başla ve uygulamanız olgunlaştıkça integration/E2E testler ekle. Mükemmel iyinin düşmanıdır.

Serverless ile çalışmak bana test stratejisinin testlerin kendisi kadar önemli olduğunu öğretti. Hızlı feedback çoğu bug'ı erken yakalar, stratejik integration testing ise sadece servisler gerçekten etkileşime girdiğinde ortaya çıkan sorunları yakalar. Anahtar, ekibiniz ve uygulamanız için doğru dengeyi bulmak.

İlgili Yazılar