Lesson 26-Serverless Testing and Debugging

Serverless Testing Strategies

Unit Testing Functions

Unit Testing Core Principles:

  • Test individual function logic
  • Isolate external dependencies
  • Execute quickly
  • Achieve high coverage

Testing Framework Choices:

  • JavaScript/TypeScript: Jest, Mocha+Chai
  • Python: pytest
  • Java: JUnit

Testing Example (Jest):

// handler.test.js
const { handler } = require('../src/handler');
const AWS = require('aws-sdk-mock');

describe('Order Handler', () => {
  beforeEach(() => {
    AWS.mock('DynamoDB.DocumentClient', 'get', (params, callback) => {
      callback(null, { Item: { id: '123', name: 'Test Order' } });
    });
  });

  afterEach(() => {
    AWS.restore('DynamoDB.DocumentClient');
  });

  it('should return order details', async () => {
    const event = { pathParameters: { id: '123' } };
    const result = await handler(event);
    
    expect(result.statusCode).toBe(200);
    expect(JSON.parse(result.body).name).toBe('Test Order');
  });
});

Testing Techniques:

  1. Dependency Isolation:
    • Use mock libraries to replace real services
    • Override environment variables
  2. Testing Layers:
graph TD
A[Unit Tests] --> B[Function Logic]
A --> C[Input Validation]
A --> D[Error Handling]

E[Integration Tests] --> F[API Gateway]
E --> G[Database Interaction]
E --> H[Event Triggers]

I[End-to-End Tests] --> J[Complete User Flow]
  1. Test Coverage:
# Jest configuration
"jest": {
  "collectCoverage": true,
  "coverageThreshold": {
    "global": {
      "branches": 80,
      "functions": 80,
      "lines": 80,
      "statements": 80
    }
  }
}

Integration Testing and Event Simulation

Integration Testing Focus:

  • Inter-function interactions
  • External service integrations
  • Data flow validation

Event Simulation Tools:

  1. AWS EventBridge Simulation:
aws events put-events --entries '[
  {
    "Source": "test.source",
    "DetailType": "TestEvent",
    "Detail": "{\"key\":\"value\"}",
    "EventBusName": "default"
  }
]'
  1. S3 Event Simulation:
// Simulate S3 event
const s3Event = {
  Records: [{
    s3: {
      bucket: { name: 'test-bucket' },
      object: { key: 'test-file.txt' }
    }
  }]
};
  1. DynamoDB Stream Simulation:
{
  "Records": [
    {
      "eventID": "1",
      "eventName": "INSERT",
      "dynamodb": {
        "NewImage": {
          "id": {"S": "123"},
          "name": {"S": "Test Item"}
        }
      }
    }
  ]
}

Integration Testing Example:

const AWS = require('aws-sdk');
const { handler } = require('../src/handler');

describe('Integration Tests', () => {
  beforeAll(async () => {
    // Initialize test data
    await DynamoDB.putItem({ TableName: 'TestTable', Item: { id: '1', name: 'Test' } }).promise();
  });

  afterAll(async () => {
    // Clean up test data
    await DynamoDB.deleteItem({ TableName: 'TestTable', Key: { id: '1' } }).promise();
  });

  it('should process S3 event correctly', async () => {
    const event = require('./events/s3-event.json');
    const response = await handler(event);
    
    expect(response).toBeDefined();
    // Verify database changes
    const data = await DynamoDB.getItem({ TableName: 'TestTable', Key: { id: '1' } }).promise();
    expect(data.Item.name.S).toBe('Processed');
  });
});

End-to-End Testing

E2E Testing Framework Choices:

  • REST API: Postman, Newman, Supertest
  • Full-Stack Apps: Cypress, Playwright
  • Serverless-Specific: Serverless Artillery

Testing Process:

  1. Deploy test environment
  2. Prepare test data
  3. Execute user operation sequences
  4. Verify system state
  5. Clean up test data

Example (Cypress):

describe('Order Process', () => {
  it('should complete order successfully', () => {
    // 1. Visit product page
    cy.visit('/products/123');
    
    // 2. Add to cart
    cy.get('.add-to-cart').click();
    
    // 3. Checkout
    cy.get('.checkout').click();
    cy.fillForm({
      name: 'Test User',
      address: '123 Test St'
    });
    cy.get('.submit-order').click();
    
    // 4. Verify order confirmation
    cy.contains('Order Confirmed').should('be.visible');
    cy.get('.order-id').then(($el) => {
      const orderId = $el.text();
      // Verify database
      cy.verifyOrderInDB(orderId);
    });
  });
});

E2E Testing Strategies:

  1. Contract Testing: Verify APIs conform to contracts
  2. Scenario Testing: Cover main user journeys
  3. Performance Baseline: Establish performance benchmarks
  4. Regression Protection: Ensure new changes don’t break existing functionality

Serverless Debugging Techniques

Cloud Logs and Tracing

CloudWatch Logs:

# View function logs
aws logs tail /aws/lambda/my-function --follow

# Search specific logs
aws logs filter-log-events \
  --log-group-name /aws/lambda/my-function \
  --filter-pattern "ERROR" \
  --start-time 1620000000000 \
  --end-time 1620003600000

X-Ray Tracing:

const AWSXRay = require('aws-xray-sdk');
AWSXRay.captureAWS(require('aws-sdk'));

exports.handler = async (event) => {
  const segment = new AWSXRay.Segment('handler');
  
  try {
    // Record subsegment
    const dbSegment = segment.addNewSubsegment('database-query');
    const data = await queryDatabase();
    dbSegment.close();
    
    return { data };
  } catch (err) {
    segment.addError(err);
    throw err;
  } finally {
    segment.close();
  }
};

Log Enhancement Techniques:

  1. Structured Logging:
logger.info('Order processed', {
  orderId: '123',
  status: 'shipped',
  timestamp: new Date().toISOString()
});
  1. Correlation ID:
// Generate correlation ID
const correlationId = uuid.v4();
process.env.CORRELATION_ID = correlationId;

// Include correlation ID in all logs
logger.info('Processing request', { correlationId });

Local Debugging Tools

Serverless Offline:

serverless offline start --port 3000

SAM CLI:

sam local start-api --env-vars env.json

Debug Configuration (Visual Studio Code):

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Lambda",
      "program": "${workspaceFolder}/node_modules/serverless/bin/serverless",
      "args": [
        "offline",
        "start",
        "--noTimeout",
        "--port", "3000"
      ],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/**/*.js"]
    }
  ]
}

Local Simulation Tools:

  • LocalStack: Simulates AWS services
  • DynamoDB Local: Local DynamoDB
  • S3 Proxy: Local S3 simulation

Error Handling and Retries

Error Classification:

  1. Transient Errors: Network issues, temporary service unavailability
  2. Recoverable Errors: Validation failures, business rule conflicts
  3. Non-Recoverable Errors: Programming errors, invalid states

Retry Strategy:

async function withRetry(fn, retries = 3, delay = 1000) {
  try {
    return await fn();
  } catch (err) {
    if (retries <= 0 || !isTransientError(err)) throw err;
    
    await new Promise(resolve => setTimeout(resolve, delay));
    return withRetry(fn, retries - 1, delay * 2);
  }
}

function isTransientError(err) {
  return err.statusCode === 500 || 
         err.code === 'ProvisionedThroughputExceededException' ||
         err.message.includes('Timeout');
}

Dead Letter Queue (DLQ) Configuration:

functions:
  processOrder:
    handler: handler.processOrder
    events:
      - sqs:
          arn: arn:aws:sqs:us-east-1:123456789012:orders-queue
          batchSize: 10
    onError: arn:aws:sqs:us-east-1:123456789012:dead-letter-queue

Testing Tools and Frameworks

Jest in Serverless

Jest Configuration:

// jest.config.js
module.exports = {
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  collectCoverageFrom: ['src/**/*.{js,ts}'],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  }
};

Jest Mock Example:

// __mocks__/aws-sdk.js
const mockDynamoDB = {
  get: jest.fn().mockImplementation((params, callback) => {
    callback(null, { Item: { id: '1', name: 'Mock Item' } });
  })
};

const AWS = {
  DynamoDB: {
    DocumentClient: jest.fn(() => mockDynamoDB)
  }
};

module.exports = AWS;

Test Coverage Report:

jest --coverage --reporters=default --reporters=jest-junit

SAM Local Testing

SAM Template Configuration:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Local Testing Commands:

# Start local service
sam local start-api --env-vars env.json

# Invoke function directly for testing
sam local invoke "HelloWorldFunction" -e event.json

# Generate CloudFormation template
sam package --template-file template.yaml --s3-bucket my-bucket --output-template-file packaged.yaml

Serverless Artillery Performance Testing

Installation and Configuration:

npm install -g serverless-artillery

Test Scenario Definition:

# load-test.yml
config:
  target: "https://api.example.com"
  phases:
    - duration: 60
      arrivalRate: 10
scenarios:
  - name: "Get Orders"
    flow:
      - get:
          url: "/orders"

Execute Test:

slsart invoke --stage test --path load-test.yml

Result Analysis:

{
  "totalRequests": 600,
  "failures": 15,
  "latency": {
    "min": 50,
    "max": 300,
    "median": 120,
    "p95": 200
  }
}

Advanced Features:

  1. Distributed Testing:
slsart invoke --stage test --path load-test.yml --workers 5
  1. Custom Metrics:
scenarios:
  - name: "Order Creation"
    flow:
      - post:
          url: "/orders"
          json:
            productId: 123
            quantity: 2
      - think: 1
      - extract:
          - name: "orderId"
            regexp: '"id":"(.*?)"'
  1. CI/CD Integration:
# .github/workflows/load-test.yml
name: Load Test

on:
  pull_request:
    branches: [ main ]

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: npm install -g serverless-artillery
      - run: slsart invoke --stage test --path load-test.yml
Share your love