Skip to content

add a testing harness to ease testing of lambdas #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
10 changes: 10 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ let package = Package(
.macOS(.v10_13),
],
products: [
// core library
.library(name: "AWSLambdaRuntime", targets: ["AWSLambdaRuntime"]),
// common AWS events
.library(name: "AWSLambdaEvents", targets: ["AWSLambdaEvents"]),
// for testing only
.library(name: "AWSLambdaTesting", targets: ["AWSLambdaTesting"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"),
Expand All @@ -26,6 +30,12 @@ let package = Package(
.testTarget(name: "AWSLambdaRuntimeTests", dependencies: ["AWSLambdaRuntime"]),
.target(name: "AWSLambdaEvents", dependencies: []),
.testTarget(name: "AWSLambdaEventsTests", dependencies: ["AWSLambdaEvents"]),
// testing helper
.target(name: "AWSLambdaTesting", dependencies: [
"AWSLambdaRuntime",
.product(name: "NIOHTTP1", package: "swift-nio"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can see the dependency is NIO and not NIOHTTP1.

]),
.testTarget(name: "AWSLambdaTestingTests", dependencies: ["AWSLambdaTesting"]),
// samples
.target(name: "StringSample", dependencies: ["AWSLambdaRuntime"]),
.target(name: "CodableSample", dependencies: ["AWSLambdaRuntime"]),
Expand Down
61 changes: 61 additions & 0 deletions Sources/AWSLambdaTesting/Lambda+Testing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

// @testable for access of internal functions - this would only work for testing by design
@testable import AWSLambdaRuntime
import Dispatch
import Logging
import NIO

extension Lambda {
public static func test(_ closure: @escaping StringLambdaClosure,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very excited about this API design. 😍

with payload: String,
_ body: @escaping (Result<String, Error>) -> Void) {
Self.test(StringLambdaClosureWrapper(closure), with: payload, body)
}

public static func test(_ closure: @escaping StringVoidLambdaClosure,
with payload: String,
_ body: @escaping (Result<Void, Error>) -> Void) {
Self.test(StringVoidLambdaClosureWrapper(closure), with: payload, body)
}

public static func test<In: Decodable, Out: Encodable>(_ closure: @escaping CodableLambdaClosure<In, Out>,
with payload: In,
_ body: @escaping (Result<Out, Error>) -> Void) {
Self.test(CodableLambdaClosureWrapper(closure), with: payload, body)
}

public static func test<In: Decodable>(_ closure: @escaping CodableVoidLambdaClosure<In>,
with payload: In,
_ body: @escaping (Result<Void, Error>) -> Void) {
Self.test(CodableVoidLambdaClosureWrapper(closure), with: payload, body)
}

public static func test<In, Out, Handler: EventLoopLambdaHandler>(_ handler: Handler,
with payload: In,
_ body: @escaping (Result<Out, Error>) -> Void) where Handler.In == In, Handler.Out == Out {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to work with completion blocks here?

I would be in favor of a sync api, which returns an Out and throws:

public static func test<In, Out, Handler: EventLoopLambdaHandler>(
  _ handler: Handler,
  with payload: In) throws 
  -> Out where Handler.In == In, Handler.Out == Out 

Going for such a sync approach would safe developers from using XCTestExpectation, which should make testing code much nicer.

This change should trickle down to all other methods of course.

let logger = Logger(label: "test")
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going for a sync approach, will allow us to defer { eventLoopGroup.syncShutdown() }

let context = Context(requestId: "\(DispatchTime.now().uptimeNanoseconds)",
traceId: "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1",
invokedFunctionArn: "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime",
deadline: .now() + 5,
logger: logger,
eventLoop: eventLoopGroup.next())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we maybe want to create a

struct TestConfig {
  let requestId: String
  let traceId: String
  let invokedFunctionArn: String
  let timeout: Double
}

that can be optionally injected into the Lambda.test method, which could allow developers to test Timeout handlers and so on.

handler.handle(context: context, payload: payload).whenComplete { result in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we execute this on the eventLoop as it would happen in the real environment?

body(result)
}
}
}
114 changes: 114 additions & 0 deletions Tests/AWSLambdaTestingTests/Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import AWSLambdaRuntime
import AWSLambdaTesting
import XCTest

class LambdaTestingTests: XCTestCase {
func testCodableClosure() {
struct Request: Codable {
let name: String
}

struct Response: Codable {
let message: String
}

let myLambda = { (_: Lambda.Context, request: Request, callback: (Result<Response, Error>) -> Void) in
callback(.success(Response(message: "echo" + request.name)))
}

let request = Request(name: UUID().uuidString)
Lambda.test(myLambda, with: request) { result in
switch result {
case .failure(let error):
XCTFail("expected to succeed but failed with \(error)")
case .success(let response):
XCTAssertEqual(response.message, "echo" + request.name)
}
}
}

func testCodableVoidClosure() {
struct Request: Codable {
let name: String
}

let myLambda = { (_: Lambda.Context, _: Request, callback: (Result<Void, Error>) -> Void) in
callback(.success(()))
}

let request = Request(name: UUID().uuidString)
Lambda.test(myLambda, with: request) { result in
switch result {
case .failure(let error):
XCTFail("expected to succeed but failed with \(error)")
case .success:
break
}
}
}

func testLambdaHandler() {
struct Request: Codable {
let name: String
}

struct Response: Codable {
let message: String
}

struct MyLambda: LambdaHandler {
typealias In = Request
typealias Out = Response

func handle(context: Lambda.Context, payload: In, callback: @escaping (Result<Out, Error>) -> Void) {
callback(.success(Response(message: "echo" + payload.name)))
}
}

let request = Request(name: UUID().uuidString)
Lambda.test(MyLambda(), with: request) { result in
switch result {
case .failure(let error):
XCTFail("expected to succeed but failed with \(error)")
case .success(let response):
XCTAssertEqual(response.message, "echo" + request.name)
}
}
}

func testFailure() {
struct MyError: Error {}

struct MyLambda: LambdaHandler {
typealias In = String
typealias Out = Void

func handle(context: Lambda.Context, payload: In, callback: @escaping (Result<Out, Error>) -> Void) {
callback(.failure(MyError()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid all those test cases work only because the callback is immediately called.

}
}

Lambda.test(MyLambda(), with: UUID().uuidString) { result in
switch result {
case .failure(let error):
XCTAssert(error is MyError)
case .success:
XCTFail("expected to fail but succeeded")
}
}
}
}