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:
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:
- Sadece external bağımlılıkları mock’la (DynamoDB), business logic’i değil
- Business function’ı (
getUser) handler’dan ayrı test et - Sadece return değerlerini değil, gerçek AWS SDK çağrı parametrelerini doğrula
- Test event oluşturma için helper function’lar yarat
- 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’ü:
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
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.
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 CDK, Lambda ve GitHub Actions kullanarak otomatik preview ortamları oluşturmayı öğrenin - sorunsuz PR test ve inceleme süreçleri için
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ı.
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.