İçeriğe atla

2025-12-06

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 uygulamaları deploy döngüsünü dakikalara sıkıştırır; bu, test ekonomisini değiştirir: eskiden uzun sürüm süreciyle yakalanan bir hata artık bir insan incelemesinden önce production’a ulaşır. Test stratejisi, deploy hızının artık yakalayamadığı şeyi yakalamak zorundadır. Aynı zamanda serverless mimarisi yerel öncelikli testi kırar; Lambda cold start’ları, IAM izinleri, event şemaları ve yönetilen servisler arasındaki sınırlar yerelde canlı bir hesaptakinden farklı davranır.

Bu yazı, AWS üzerinde serverless uygulamalar için bir test stratejisini ele alır. Katmanlı yaklaşımı (unit, LocalStack ya da canlı hesaba karşı integration, contract, end-to-end), mock’ların gizlediği spesifik hata biçimlerini ve test setini deploy hızıyla orantılı tutan CI desenlerini kapsar.

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:

E2E Testler - 10%

Full cloud deployment

5-10 dakika

Yüksek AWS maliyeti

Integration Testler - 20%

Gerçek veya local AWS servisleri

2-5 dakika

Orta maliyet

Unit Testler - 70%

AWS çağırısı yok, mock bağımlılıklar

10-30 saniye

AWS maliyeti yok

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:

// Kötü: Her şey handler içinde
export 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ı sorumluluklar
export 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:

// user-handler.ts
import { 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:

// user-handler.test.ts
import { 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ı fonksiyon
function 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:

// user-repository.integration.test.ts
import { 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ı kullan
const 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’ü:

Evet

Hayır

Step Functions

veya EventBridge

Hayır

Evet

Hayır

Evet

Hayır

Integration Test Gerekli

IAM izinlerini

test ediyor musun?

Gerçek AWS Kullan

Servis

orchestration test

ediyor musun?

Basit CRUD

operasyonları mı?

LocalStack Kullan

Karmaşık query'ler

veya transaction'lar mı?

Hızlı feedback

AWS maliyeti yok

Production doğruluğu

Gerçek sorunları yakalar

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:

// api-integration.test.ts
import { 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:

# Local API Gateway başlat
sam local start-api --port 3000

# Başka bir terminal'de curl ile test et
curl -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ğır
sam local invoke UserFunction --event events/create-user.json

events/create-user.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

// state-machine.test.ts
import * 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

// step-functions-integration.test.ts
import {
  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:

// eventbridge-integration.test.ts
import { 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:

# .github/workflows/test.yml
name: 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:

{
  "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.

// KÖTÜ: Test geçer, production'da başarısız olur
ddbMock.on(PutCommand).resolves({});
// Deploy → "AccessDenied: User is not authorized to perform: dynamodb:PutItem"

// İYİ: Gerçek DynamoDB ile integration test bunu yakalar
const 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.

// KÖTÜ: Eksik test event
const event = { body: JSON.stringify({ id: 1 }) };

// İYİ: @types/aws-lambda kullan
import { 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.

// KÖTÜ: Event henüz işlenmedi
await eventBridge.putEvents({ Entries: [event] });
const result = await queryResults(); // Boş!

// İYİ: İşlenmeyi bekle
await eventBridge.putEvents({ Entries: [event] });
await waitForEventProcessing(); // Poll et veya Step Functions kullan
const 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.

// KÖTÜ: Paylaşılan resource'lar
const 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 kullan
afterEach(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

Pact ile Contract Testing - Microservislerde API Uyumluluğunu Sağlama

TypeScript microservislerde consumer-driven contract testing'i Pact ile uygulamaya yönelik pratik bir kılavuz. Breaking API değişikliklerini deployment öncesi yakalayın ve integration test yükünü azaltın.

testingmicroservicesapi+7
AWS CDK Link Kısaltıcı Bölüm 1: Proje Kurulumu & Temel Altyapı

AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Gerçek mimari kararlar, ilk kurulum ve büyük ölçekte URL kısaltıcıları inşa etmenin dersleri.

aws-cdklambdadynamodb+6
AWS CDK ve Serverless ile Geçici Preview Ortamları Oluşturma

AWS CDK, Lambda ve GitHub Actions kullanarak otomatik preview ortamları oluşturmayı öğrenin - sorunsuz PR test ve inceleme süreçleri için

aws-cdkserverlessci-cd+5
AWS Serverless TypeScript Projelerini Yapılandırma: Eksiksiz Rehber

AWS Lambda, API Gateway ve TypeScript ile production-ready serverless projeleri oluşturmak için en iyi uygulamalar. Gerçek dünya örnekleri, maliyet optimizasyonu ve performans ipuçları.

api-gatewaycost-optimizationlambda+2
Amazon Cognito Derinlemesine: Temel Authentication'ın Ötesinde

Amazon Cognito'nun gelişmiş özellikleri üzerine kapsamlı teknik kılavuz: özel authentication akışları, federation pattern'leri, multi-tenancy mimarileri, migration stratejileri ve production-grade güvenlik implementasyonu.

awscognitoauthentication+7