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.
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. Serverless mimarisi yerel öncelikli testi de 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 boşluğun büyük kısmını dört hata biçimi doldurur: mock’lanmış AWS SDK çağrılarıyla geçen ama production’da AccessDenied alan testler, LocalStack’in kabul edip gerçek AWS’nin reddettiği çağrılar, assertion’lar iş bitmeden çalıştığı için güvenilmez olan asenkron EventBridge ve Step Functions teslimatı ve tek bir handler değişikliğini görmek için beklenen dakikalarca süren CloudFormation deploy’ları. Bunlara cevap veren katmanlama şu: business logic’i SDK mock’lanmış halde unit test et, IAM veya servis orkestrasyonu işin içindeyse integration testlerini canlı bir AWS hesabına karşı çalıştır, LocalStack’i deploy öncesi doğrulama için değil yerel iterasyon için sakla.
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.
Test Seviyesi Seçimi#
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: 'ahmet@example.com' }
});
const user = await getUser('123');
expect(user).toEqual({
PK: 'USER#123',
name: 'Ahmet',
email: 'ahmet@example.com'
});
});
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 - AWS SDK çağrı parametrelerine assertion yaz; yanlış key yapısı da geçerli görünen bir dönüş değeri üretir
- 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: 'ahmet@example.com',
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: 'ahmet@example.com'
});
});
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 ve Gerçek AWS: Kullanım Senaryoları#
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.
Pratik bir 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: 'ahmet@example.com'
}),
// ... 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":"ahmet@example.com"}'
# 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\":\"ahmet@example.com\"}"
}
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 Tanımını Doğrulama#
// state-machine.test.ts
import * as fs from 'fs';
import * as path from 'path';
const definitionPath = path.join(__dirname, 'state-machine.asl.json');
const loadDefinition = () => JSON.parse(fs.readFileSync(definitionPath, 'utf8'));
describe('State Machine Definition', () => {
it('geçerli JSON syntax\'ına sahip', () => {
const definition = loadDefinition();
expect(definition).toHaveProperty('StartAt');
expect(definition).toHaveProperty('States');
});
it('her task state için error handling var', () => {
const definition = loadDefinition();
// Hatalı state'leri topla ki başarısızlık mesajı onları isimleriyle göstersin
const tasksWithoutCatch = Object.entries(definition.States)
.filter(([, state]: [string, any]) => state.Type === 'Task' && !state.Catch)
.map(([name]) => name);
expect(tasksWithoutCatch).toEqual([]);
});
});
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 { DynamoDBClient } from '@aws-sdk/client-dynamodb';
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: 'test@example.com',
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@v5
# 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: koşu başına bir stack deploy ve teardown
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.
Dağılım Nerede Geçerli#
%70/%20/%10 dağılımı, deploy sık ve kötü bir deploy’u geri almak ucuzken geçerlidir: hacmi hızlı unit testler taşır, gerçek AWS’ye karşı çalışan daha küçük bir integration katmanı IAM ve orkestrasyonu kapsar, E2E koşuları main branch’i korur. Bir hatayı geri almak pahalıysa (ödeme, veri migrasyonu, regülasyon izi olan her şey) ya da davranışın çoğu uygulama kodunda değil servis konfigürasyonunda yaşıyorsa ağırlığı integration ve E2E tarafına kaydır. Uygulama az sayıda servis sınırı olan saf dönüşümlerden oluşuyorsa tersine kaydır; orada fazladan bir E2E koşusu her merge’de dakikalara mal olur ve unit testin yakalayacağından fazlasını yakalamaz.
Dağılım ne olursa olsun iki kural değişmez. IAM gerçek bir hesaba karşı çalıştırılır, çünkü mock, deploy edilmiş rolün reddedeceği bir çağrıyı onaylar. Ve asenkron bir side effect’e, önce poll etmeden assertion yazılmaz.
Kaynaklar#
- AWS Lambda Test Kılavuzu (yeni sekmede açılır) - İş mantığını handler kodundan ayırma ve unit test stratejileri dahil Lambda fonksiyonlarını test etmeye yönelik resmi AWS belgesi.
- AWS SAM ile Serverless Uygulamanızı Test Edin (yeni sekmede açılır) - AWS’ye deploy etmeden önce yerel test ve hata ayıklama için resmi AWS SAM kılavuzu.
- LocalStack Lambda Belgesi (yeni sekmede açılır) - Yerel Lambda emülasyonunu, desteklenen API’leri ve gerçek AWS ile bilinen sınırlamaları ele alan resmi LocalStack belgesi.
- AWS SAM ile Yerel Integration Testlerini Otomatikleştirme (yeni sekmede açılır) - CI pipeline’larında sam local kullanarak integration testlerini otomatikleştirme hakkında AWS kılavuzu.
- Jest: Başlarken (yeni sekmede açılır) - JavaScript ve TypeScript projeleri için kurulum, test çalıştırıcıları, mock’lama ve kod kapsamını kapsayan resmi Jest belgesi.
- sam local invoke ile Test Etmeye Giriş (yeni sekmede açılır) - SAM CLI kullanarak Lambda fonksiyonlarını test olaylarıyla yerel olarak çağırmaya yönelik resmi AWS referansı.
- AWS Well-Architected: Serverless Uygulamalar Lens (yeni sekmede açılır) - Hata modlarını test etmek açısından ilgili, serverless mimarilere yönelik AWS güvenilirlik ve operasyon kılavuzu.
İlgili yazılar
AWS CDK, DynamoDB ve Lambda ile production-grade link kısaltıcı kurulumu. Mimari kararlar, proje yapısı ve ölçekte ayakta kalan şema seçimleri.
aws-cdk · lambda · dynamodb +5
Distributed sistemlerde feature flag için production rehberi: LaunchDarkly, Unleash ve AWS AppConfig karşılaştırması, rollout ve A/B testing örnekleri.
feature-flags · devops · ci-cd +5
Git'te secret'ları şifrelemek için pratik Mozilla SOPS rehberi: age encryption, AWS CDK ve Lambda pattern'leri, production-ready serverless güvenlik.
secrets-management · ci-cd · terraform +7
TypeScript microservislerde Pact ile consumer-driven contract testing: breaking API değişikliklerini deployment öncesi yakalayın, integration test yükünü azaltın.
testing · microservices · api-design +3
AWS CDK, Lambda ve GitHub Actions kullanarak otomatik preview ortamları oluşturmayı öğrenin - sorunsuz PR test ve inceleme süreçleri için
aws-cdk · serverless · ci-cd +5