|
| 1 | +# Swift Testing Example |
| 2 | + |
| 3 | +This is a simple example to show different testing strategies for your Swift Lambda functions. |
| 4 | +For this example, we developed a simple Lambda function that returns the body of the API Gateway payload in lowercase, except for the first letter, which is in uppercase. |
| 5 | + |
| 6 | +In this document, we describe four different testing strategies: |
| 7 | + * [Unit Testing your business logic](#unit-testing-your-business-logic) |
| 8 | + * [Integration testing the handler function](#integration-testing-the-handler-function) |
| 9 | + * [Local invocation using the Swift AWS Lambda Runtime](#local-invocation-using-the-swift-aws-lambda-runtime) |
| 10 | + * [Local invocation using the AWS SAM CLI](#local-invocation-using-the-aws-sam-cli) |
| 11 | + |
| 12 | +> [!IMPORTANT] |
| 13 | +> In this example, the API Gateway sends a payload to the Lambda function as a JSON string. Your business payload is in the `body` section of the API Gateway payload. It is base64-encoded. You can find an example of the API Gateway payload in the `event.json` file. The API Gateway event format is documented in [Create AWS Lambda proxy integrations for HTTP APIs in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html). |
| 14 | +
|
| 15 | +To include a sample event in your test targets, you must add the `event.json` file from the `Tests` directory to the binary bundle. To do so, add a `resources` section in your `Package.swift` file: |
| 16 | + |
| 17 | +```swift |
| 18 | + .testTarget( |
| 19 | + name: "LambdaFunctionTests", |
| 20 | + dependencies: ["APIGatewayLambda"], |
| 21 | + path: "Tests", |
| 22 | + resources: [ |
| 23 | + .process("event.json") |
| 24 | + ] |
| 25 | + ) |
| 26 | +``` |
| 27 | + |
| 28 | +## Unit Testing your business logic |
| 29 | + |
| 30 | +You can test the business logic of your Lambda function by writing unit tests for your business code used in the handler function, just like usual. |
| 31 | + |
| 32 | +1. Create your Swift Test code in the `Tests` directory. |
| 33 | + |
| 34 | +```swift |
| 35 | +let valuesToTest: [(String, String)] = [ |
| 36 | + ("hello world", "Hello world"), // happy path |
| 37 | + ("", ""), // Empty string |
| 38 | + ("a", "A"), // Single character |
| 39 | +] |
| 40 | + |
| 41 | +@Suite("Business Tests") |
| 42 | +class BusinessTests { |
| 43 | + |
| 44 | + @Test("Uppercased First", arguments: valuesToTest) |
| 45 | + func uppercasedFirst(_ arg: (String,String)) { |
| 46 | + let input = arg.0 |
| 47 | + let expectedOutput = arg.1 |
| 48 | + #expect(input.uppercasedFirst() == expectedOutput) |
| 49 | + } |
| 50 | +}``` |
| 51 | + |
| 52 | +2. Add a test target to your `Package.swift` file. |
| 53 | +```swift |
| 54 | + .testTarget( |
| 55 | + name: "BusinessTests", |
| 56 | + dependencies: ["APIGatewayLambda"], |
| 57 | + path: "Tests" |
| 58 | + ) |
| 59 | +``` |
| 60 | + |
| 61 | +3. run `swift test` to run the tests. |
| 62 | + |
| 63 | +## Integration Testing the handler function |
| 64 | + |
| 65 | +You can test the handler function by creating an input event, a mock Lambda context, and calling the handler function from your test. |
| 66 | +Your Lambda handler function must be declared separatly from the `LambdaRuntime`. For example: |
| 67 | + |
| 68 | +```swift |
| 69 | +public struct MyHandler: Sendable { |
| 70 | + |
| 71 | + public func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { |
| 72 | + context.logger.debug("HTTP API Message received") |
| 73 | + context.logger.trace("Event: \(event)") |
| 74 | + |
| 75 | + var header = HTTPHeaders() |
| 76 | + header["content-type"] = "application/json" |
| 77 | + |
| 78 | + if let payload = event.body { |
| 79 | + // call our business code to process the payload and return a response |
| 80 | + return APIGatewayV2Response(statusCode: .ok, headers: header, body: payload.uppercasedFirst()) |
| 81 | + } else { |
| 82 | + return APIGatewayV2Response(statusCode: .badRequest) |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +let runtime = LambdaRuntime(body: MyHandler().handler) |
| 88 | +try await runtime.run() |
| 89 | +``` |
| 90 | + |
| 91 | +Then, the test looks like this: |
| 92 | + |
| 93 | +```swift |
| 94 | +@Suite("Handler Tests") |
| 95 | +public struct HandlerTest { |
| 96 | + |
| 97 | + @Test("Invoke handler") |
| 98 | + public func invokeHandler() async throws { |
| 99 | + |
| 100 | + // read event.json file |
| 101 | + let testBundle = Bundle.module |
| 102 | + guard let eventURL = testBundle.url(forResource: "event", withExtension: "json") else { |
| 103 | + Issue.record("event.json not found in test bundle") |
| 104 | + return |
| 105 | + } |
| 106 | + let eventData = try Data(contentsOf: eventURL) |
| 107 | + |
| 108 | + // decode the event |
| 109 | + let apiGatewayRequest = try JSONDecoder().decode(APIGatewayV2Request.self, from: eventData) |
| 110 | + |
| 111 | + // create a mock LambdaContext |
| 112 | + let lambdaContext = LambdaContext.__forTestsOnly( |
| 113 | + requestID: UUID().uuidString, |
| 114 | + traceID: UUID().uuidString, |
| 115 | + invokedFunctionARN: "arn:", |
| 116 | + timeout: .milliseconds(6000), |
| 117 | + logger: Logger(label: "fakeContext") |
| 118 | + ) |
| 119 | + |
| 120 | + // call the handler with the event and context |
| 121 | + let response = try await MyHandler().handler(event: apiGatewayRequest, context: lambdaContext) |
| 122 | + |
| 123 | + // assert the response |
| 124 | + #expect(response.statusCode == .ok) |
| 125 | + #expect(response.body == "Hello world of swift lambda!") |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +## Local invocation using the Swift AWS Lambda Runtime |
| 131 | + |
| 132 | +You can test your Lambda function locally by invoking it with the Swift AWS Lambda Runtime. |
| 133 | + |
| 134 | +You must pass a payload to the Lambda function. You can use the `Tests/event.json` file for this purpose. The return value is a `APIGatewayV2Response` object in this example. |
| 135 | + |
| 136 | +Just type `swift run` to run the Lambda function locally. |
| 137 | + |
| 138 | +```sh |
| 139 | +LOG_LEVEL=trace swift run |
| 140 | + |
| 141 | +# from another terminal |
| 142 | +# the `-X POST` flag is implied when using `--data`. It is here for clarity only. |
| 143 | +curl -X POST "http://127.0.0.1:7000/invoke" --data @Tests/event.json |
| 144 | +``` |
| 145 | + |
| 146 | +This returns the following response: |
| 147 | + |
| 148 | +```text |
| 149 | +{"statusCode":200,"headers":{"content-type":"application\/json"},"body":"Hello world of swift lambda!"} |
| 150 | +``` |
| 151 | + |
| 152 | +## Local invocation using the AWS SAM CLI |
| 153 | + |
| 154 | +The AWS SAM CLI provides you with a local testing environment for your Lambda functions. It deploys and invoke your function locally in a Docker container designed to mimic the AWS Lambda environment. |
| 155 | + |
| 156 | +You must pass a payload to the Lambda function. You can use the `event.json` file for this purpose. The return value is a `APIGatewayV2Response` object in this example. |
| 157 | + |
| 158 | +```sh |
| 159 | +sam local invoke -e Tests/event.json |
| 160 | + |
| 161 | +START RequestId: 3270171f-46d3-45f9-9bb6-3c2e5e9dc625 Version: $LATEST |
| 162 | +2024-12-21T16:49:31+0000 debug LambdaRuntime : [AWSLambdaRuntimeCore] LambdaRuntime initialized |
| 163 | +2024-12-21T16:49:31+0000 trace LambdaRuntime : lambda_ip=127.0.0.1 lambda_port=9001 [AWSLambdaRuntimeCore] Connection to control plane created |
| 164 | +2024-12-21T16:49:31+0000 debug LambdaRuntime : [APIGatewayLambda] HTTP API Message received |
| 165 | +2024-12-21T16:49:31+0000 trace LambdaRuntime : [APIGatewayLambda] Event: APIGatewayV2Request(version: "2.0", routeKey: "$default", rawPath: "/", rawQueryString: "", cookies: [], headers: ["x-forwarded-proto": "https", "host": "a5q74es3k2.execute-api.us-east-1.amazonaws.com", "content-length": "0", "x-forwarded-for": "81.0.0.43", "accept": "*/*", "x-amzn-trace-id": "Root=1-66fb03de-07533930192eaf5f540db0cb", "x-forwarded-port": "443", "user-agent": "curl/8.7.1"], queryStringParameters: [:], pathParameters: [:], context: AWSLambdaEvents.APIGatewayV2Request.Context(accountId: "012345678901", apiId: "a5q74es3k2", domainName: "a5q74es3k2.execute-api.us-east-1.amazonaws.com", domainPrefix: "a5q74es3k2", stage: "$default", requestId: "e72KxgsRoAMEMSA=", http: AWSLambdaEvents.APIGatewayV2Request.Context.HTTP(method: GET, path: "/", protocol: "HTTP/1.1", sourceIp: "81.0.0.43", userAgent: "curl/8.7.1"), authorizer: nil, authentication: nil, time: "30/Sep/2024:20:02:38 +0000", timeEpoch: 1727726558220), stageVariables: [:], body: Optional("aGVsbG8gd29ybGQgb2YgU1dJRlQgTEFNQkRBIQ=="), isBase64Encoded: false) |
| 166 | +END RequestId: 5b71587a-39da-445e-855d-27a700e57efd |
| 167 | +REPORT RequestId: 5b71587a-39da-445e-855d-27a700e57efd Init Duration: 0.04 ms Duration: 21.57 ms Billed Duration: 22 ms Memory Size: 512 MB Max Memory Used: 512 MB |
| 168 | + |
| 169 | +{"body": "Hello world of swift lambda!", "statusCode": 200, "headers": {"content-type": "application/json"}} |
| 170 | +``` |
0 commit comments