Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions python-test-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This portion of the repository contains code samples for testing serverless appl
|[API Gateway with Lambda and DynamoDB](./apigw-lambda-dynamodb)|This project contains unit and integration tests for a pattern using API Gateway, AWS Lambda and Amazon DynamoDB.|
|[Schema and Contract Testing](./schema-and-contract-testing)|This project contains sample schema and contract tests for an event driven architecture.|
|[Kinesis with Lambda and DynamoDB](./kinesis-lambda-dynamodb)|This project contains a example of testing an application with an Amazon Kinesis Data Stream.|
|[DynamoDB CRUD with Lambda Local](./dynamodb-crud-lambda-local)|This project contains unit pytest running CRUD operations using lambda functions and DynamoDB on local Docker containers.|
|[SQS with Lambda](./apigw-sqs-lambda-sqs)|This project demonstrates testing SQS as a source and destination in an integration test|
|[Step Functions Local](./step-functions-local)| An example of testing Step Functions workflow locally using pytest and Testcontainers |

Expand Down
194 changes: 194 additions & 0 deletions python-test-samples/dynamodb-crud-lambda-local/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Testing Workflow - PyTest que Replica SAM CLI Manual

## 🚀 Setup y Ejecución (Workflow Limpio)

### **Prerequisitos**
- Docker running
- AWS SAM CLI instalado
- Python 3.10+

### **1. Preparar el Entorno**

```bash
# Navegar al directorio del proyecto
cd dynamodb-crud-lambda-local

# Build SAM application
cd tests
sam build

# Verificar build exitoso
ls .aws-sam/build/ # Debería mostrar las 5 funciones Lambda
```

### **2. Iniciar DynamoDB Local (FUERA de PyTest)**

```bash
# Iniciar DynamoDB Local con network host
docker run --rm -d --name dynamodb-local --network host amazon/dynamodb-local

# Verificar que esté corriendo
curl http://localhost:8000/ # Debería responder
```

### **3. Configurar Variables de Entorno**

```bash
# En el directorio tests/
export AWS_ACCESS_KEY_ID='DUMMYIDEXAMPLE'
export AWS_SECRET_ACCESS_KEY='DUMMYEXAMPLEKEY'
export AWS_REGION='us-east-1'
```

### **4. Setup Python Environment**

```bash
# Crear y activar virtual environment
python3 -m venv venv
source venv/bin/activate

# Instalar dependencias
pip install --upgrade pip
pip install -r requirements.txt
```

### **5. Ejecutar Tests**

```bash
# Ejecutar todos los tests
python3 -m pytest -s unit/src/test_lambda_dynamodb_local.py

# Ejecutar test específico
python3 -m pytest -s unit/src/test_lambda_dynamodb_local.py::test_lambda_create_function

# Ejecutar con verbose output
python3 -m pytest -s -v unit/src/test_lambda_dynamodb_local.py
```

### **6. Cleanup**

```bash
# Limpiar Python environment
deactivate
rm -rf venv/

# Limpiar variables
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_REGION

# Parar DynamoDB container
docker stop dynamodb-local
```

## 📊 Output Esperado (Limpio)

```
=================== test session starts ===================
platform linux -- Python 3.10.12, pytest-8.4.1
collected 7 items

unit/src/test_lambda_dynamodb_local.py
DynamoDB Local is running on port 8000
DynamoDB Local health check passed
No existing table 'CRUDLocalTable' found - clean start confirmed

Invoking: sam local invoke CRUDLambdaInitFunction --docker-network host --event /tmp/events/temp_CRUDLambdaInitFunction_event.json
Raw SAM output: {"statusCode": 200, "body": "CRUDLocalTable"}
✓ Lambda Init function executed successfully

Invoking: sam local invoke CRUDLambdaCreateFunction --docker-network host --event /tmp/events/temp_CRUDLambdaCreateFunction_event.json
Raw SAM output: {"statusCode": 200, "body": "{\"message\": \"Item added\", \"response\": {...}}"}
✓ Lambda Create function response: {'statusCode': 200, 'message': 'Item added'}

Invoking: sam local invoke CRUDLambdaReadFunction --docker-network host --event /tmp/events/temp_CRUDLambdaReadFunction_event.json
Raw SAM output: {"statusCode": 200, "body": "{\"name\": \"Batman\", \"Id\": \"123\"}"}
✓ Lambda Read function response: {'statusCode': 200, 'Item': {'Id': '123', 'name': 'Batman'}}

Invoking: sam local invoke CRUDLambdaUpdateFunction --docker-network host --event /tmp/events/temp_CRUDLambdaUpdateFunction_event.json
Raw SAM output: {"statusCode": 200, "body": "{\"message\": \"Item updated successfully\", \"response\": {...}}"}
✓ Lambda Update function response: {'statusCode': 200, 'message': 'Item updated successfully'}

Invoking: sam local invoke CRUDLambdaDeleteFunction --docker-network host --event /tmp/events/temp_CRUDLambdaDeleteFunction_event.json
Raw SAM output: {"statusCode": 200, "body": "{\"message\": \"Item deleted\", \"response\": {...}}"}
✓ Lambda Delete function response: {'statusCode': 200, 'message': 'Item deleted'}

✓ Full CRUD workflow completed successfully through Lambda functions
✓ Performance test completed: avg_lambda_time=1850ms, crud_operations=4

=================== 7 passed in 42.15s ===================
```

## 🛠️ Troubleshooting

### **Si DynamoDB no está disponible:**
```
SKIPPED [1] DynamoDB Local is not running on port 8000. Please start with 'docker run --rm -d --name dynamodb-local --network host amazon/dynamodb-local'
```
**Solución:** Ejecutar el comando Docker mostrado.

### **Si SAM build no está actualizado:**
```
Lambda import error: No module named 'app'. Please ensure 'sam build' has been run successfully.
```
**Solución:**
```bash
cd tests
sam build
```

### **Si hay conflictos de puertos:**
```bash
# Verificar qué está usando el puerto 8000
sudo netstat -tlnp | grep :8000

# Matar procesos si es necesario
docker stop dynamodb-local
```

### **Si hay problemas de networking:**
- Verificar que Docker esté corriendo: `docker version`
- Verificar que `--network host` esté disponible (Linux/macOS)
- En Windows, puede necesitar configuración especial

## 🔄 Re-ejecución de Tests

Los tests ahora son **completamente idempotentes**:

```bash
# Puedes ejecutar múltiples veces sin problemas
python3 -m pytest -s unit/src/test_lambda_dynamodb_local.py
python3 -m pytest -s unit/src/test_lambda_dynamodb_local.py # ✅ Funciona
python3 -m pytest -s unit/src/test_lambda_dynamodb_local.py # ✅ Funciona
```

## 📝 Diferencias vs. Versión Anterior

| Aspecto | Versión Anterior | Nueva Versión |
|---------|------------------|---------------|
| **DynamoDB Management** | PyTest maneja container | Container externo |
| **Networking** | Sin `--docker-network host` | Con `--docker-network host` |
| **Clean State** | No cleanup automático | Cleanup automático al inicio |
| **Error Handling** | Muestra todos los errores | Filtra warnings harmless |
| **Idempotency** | No idempotente | Completamente idempotente |
| **Output** | Confuso con errores | Limpio y claro |

## 🎯 Validación Manual

Para verificar que PyTest replica exactamente el comportamiento manual:

```bash
# Test manual (debería funcionar igual que PyTest)
docker run --rm -d --name dynamodb-local --network host amazon/dynamodb-local

sam local invoke CRUDLambdaInitFunction --docker-network host --event ../events/lambda-init-event.json
sam local invoke CRUDLambdaCreateFunction --docker-network host --event ../events/lambda-create-event.json
sam local invoke CRUDLambdaReadFunction --docker-network host --event ../events/lambda-read-event.json
sam local invoke CRUDLambdaUpdateFunction --docker-network host --event ../events/lambda-update-event.json
sam local invoke CRUDLambdaDeleteFunction --docker-network host --event ../events/lambda-delete-event.json

# Cleanup
docker stop dynamodb-local
```

Ambos (manual y PyTest) deberían dar **exactamente los mismos resultados**.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"body": "{\"Id\": \"123\", \"name\": \"Batman\"}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Id": "123"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"key1": "value1",
"key2": "value2",
"key3": "value3"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"Id": "123"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"body": "{\"Id\": \"123\", \"name\": \"Robin\"}"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
import json
import boto3

def lambda_handler(event, context):
# Checking if running locally
if os.environ.get('AWS_SAM_LOCAL'):
# Use local DynamoDB endpoint
dynamodb = boto3.resource('dynamodb', endpoint_url='http://172.17.0.1:8000')
else:
# Use the default DynamoDB endpoint (AWS)
dynamodb = boto3.resource('dynamodb')

# Adding an item to DynamoDB
item = json.loads(event['body'])

table = dynamodb.Table(os.environ['DYNAMODB_TABLE'])
response = table.put_item(Item=item)

return {
'statusCode': 200,
'body': json.dumps({'message': 'Item added', 'response': response})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
boto3==1.35.84
botocore==1.35.84
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os
import json
import boto3

def lambda_handler(event, context):
# Checking if running locally
if os.environ.get('AWS_SAM_LOCAL'):
# Use local DynamoDB endpoint
dynamodb = boto3.resource('dynamodb', endpoint_url='http://172.17.0.1:8000')
else:
# Use the default DynamoDB endpoint (AWS)
dynamodb = boto3.resource('dynamodb')

# Deleting item on DynamoDB
#event = json.loads(event['body'])
item_id = event['Id']

table = dynamodb.Table(os.environ['DYNAMODB_TABLE'])
response = table.delete_item(Key={'Id': item_id})

return {
'statusCode': 200,
'body': json.dumps({'message': 'Item deleted', 'response': response})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
boto3==1.35.84
botocore==1.35.84
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
import boto3

def lambda_handler(event, context):
# Check if running locally
if os.environ.get('AWS_SAM_LOCAL'):
# Use local DynamoDB endpoint
dynamodb = boto3.resource('dynamodb', endpoint_url='http://172.17.0.1:8000')
else:
# Use the default DynamoDB endpoint (AWS)
dynamodb = boto3.resource('dynamodb')

# Access your DynamoDB table
table_name = dynamodb.Table(os.environ['DYNAMODB_TABLE'])

# Define the table creation parameters
table_creation_params = {
'TableName': 'CRUDLocalTable',
'KeySchema': [
{
'AttributeName': 'Id',
'KeyType': 'HASH' # Partition key
}
],
'AttributeDefinitions': [
{
'AttributeName': 'Id',
'AttributeType': 'S' # String type
}
],
'ProvisionedThroughput': {
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
}

# Create the DynamoDB table
table = dynamodb.create_table(**table_creation_params)

# Wait until the table exists before continuing
table.wait_until_exists()

print("Table created successfully:", table.table_name)

return {
'statusCode': 200,
'body': table.table_name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
boto3==1.35.84
botocore==1.35.84
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import json
import boto3

def lambda_handler(event, context):
# Check if running locally
if os.environ.get('AWS_SAM_LOCAL'):
# Use local DynamoDB endpoint
dynamodb = boto3.resource('dynamodb', endpoint_url='http://172.17.0.1:8000')
else:
# Use the default DynamoDB endpoint (AWS)
dynamodb = boto3.resource('dynamodb')

# Access your DynamoDB table
table = dynamodb.Table(os.environ['DYNAMODB_TABLE'])

item_id = event['Id']
response = table.get_item(Key={'Id': item_id})

item = response.get('Item', {})

if item:
# Item found, return it
return {
'statusCode': 200,
'body': json.dumps(item)
}
else:
# Item not found, return a 404 status
return {
'statusCode': 404,
'body': json.dumps({'error': 'Item not found', 'message': f'No item with Id {item_id} found'})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
boto3==1.35.84
botocore==1.35.84
Loading