Skip to content

Commit 5405f25

Browse files
authored
Additional Test Helpers (#10)
* Add mocked functions * Add Logger.mock member * Fix duplicated implementation * Add Runtime.getRemainingTime() * Improve ergonomics of MockContext creation * Add context provider * E -> EnvironmentVariable * Other Es -> EnvironmentVariable * Fix type
1 parent 7c3c329 commit 5405f25

File tree

10 files changed

+368
-37
lines changed

10 files changed

+368
-37
lines changed

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ let targets: [Target] = [
4040
.target(
4141
name: "LambdaMocks",
4242
dependencies: [
43-
"LambdaExtrasCore"
43+
"LambdaExtrasCore",
44+
.product(name: "NIO", package: "swift-nio")
4445
]
4546
),
4647
.testTarget(

Sources/LambdaExtras/Extensions.swift

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,29 @@ import Foundation
1111
import LambdaExtrasCore
1212
import NIOCore
1313

14+
extension Lambda {
15+
/// Returns the value of the environment variable with the given name.
16+
///
17+
/// This method throws ``EventHandler.envError`` if a value for the given environment variable
18+
/// name is not found.
19+
///
20+
/// - Parameter name: The name of the environment variable to return.
21+
/// - Returns: The value of the given environment variable.
22+
static func env(name: String) throws -> String {
23+
guard let value = env(name) else {
24+
throw HandlerError.envError(name)
25+
}
26+
27+
return value
28+
}
29+
}
30+
1431
public extension EnvironmentValueProvider where EnvironmentVariable == String {
1532
/// Returns the value of the given environment variable.
1633
///
1734
/// - Parameter environmentVariable: The environment variable whose value should be returned.
1835
func value(for environmentVariable: EnvironmentVariable) throws -> String {
19-
guard let value = Lambda.env(environmentVariable) else {
20-
throw HandlerError.envError(environmentVariable)
21-
}
22-
23-
return value
36+
try Lambda.env(name: environmentVariable)
2437
}
2538
}
2639

@@ -29,11 +42,7 @@ public extension EnvironmentValueProvider where EnvironmentVariable: RawRepresen
2942
///
3043
/// - Parameter environmentVariable: The environment variable whose value should be returned.
3144
func value(for environmentVariable: EnvironmentVariable) throws -> String {
32-
guard let value = Lambda.env(environmentVariable.rawValue) else {
33-
throw HandlerError.envError(environmentVariable.rawValue)
34-
}
35-
36-
return value
45+
try Lambda.env(name: environmentVariable.rawValue)
3746
}
3847
}
3948

Sources/LambdaExtrasCore/Protocols/RuntimeContext.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,7 @@ public protocol RuntimeContext: Sendable {
4040

4141
/// `ByteBufferAllocator` to allocate `ByteBuffer`.
4242
var allocator: ByteBufferAllocator { get }
43+
44+
/// Returns the time remaining before the deadline.
45+
func getRemainingTime() -> TimeAmount
4346
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//
2+
// ContextProvider.swift
3+
// LambdaExtras
4+
//
5+
// Created by Mathew Gacy on 1/7/24.
6+
//
7+
8+
import Foundation
9+
import Logging
10+
import NIOCore
11+
import NIO
12+
13+
/// A helper to create and manage mock initialization and runtime contexts for testing.
14+
///
15+
/// Example usage:
16+
///
17+
/// ```swift
18+
/// final class MyHandlerTests: XCTestCase {
19+
/// var contextProvider: ContextProvider<MyEnvironment>
20+
///
21+
/// override func setUp() {
22+
/// contextProvider.setUp()
23+
/// }
24+
///
25+
/// override func tearDown() {
26+
/// XCTAssertNoThrow(try contextProvider.shutdown())
27+
/// }
28+
///
29+
/// func testMyHandler() async throws {
30+
/// let sut = try await MyHandler(context: contextProvider.makeInitializationContext())
31+
/// let actual = try await sut.handle(MockEvent(), context: contextProvider.makeContext())
32+
/// ...
33+
/// }
34+
/// }
35+
/// ```
36+
public struct ContextProvider<EnvironmentVariable> {
37+
/// The event loop group used to provide the contexts' event loops.
38+
public private(set) var eventLoopGroup: EventLoopGroup!
39+
40+
/// The event loop for the contexts.
41+
public private(set) var eventLoop: EventLoop!
42+
43+
/// The logger for the contexts.
44+
public var logger: Logger
45+
46+
/// A closure returning the value of the given environment variable.
47+
public var environmentValueProvider: @Sendable (EnvironmentVariable) throws -> String
48+
49+
/// Creates an instance.
50+
///
51+
/// - Parameter environmentValueProvider: A closure returning the value of the given
52+
/// environment variable.
53+
public init(
54+
logger: Logger = .mock,
55+
environmentValueProvider: @escaping @Sendable (EnvironmentVariable) throws -> String
56+
) {
57+
self.logger = logger
58+
self.environmentValueProvider = environmentValueProvider
59+
}
60+
61+
/// Sets up the event loop used for the provided initialization and runtime contexts.
62+
///
63+
/// Call this in your test class's `setUp()` method:
64+
///
65+
/// ```swift
66+
/// final class MyHandlerTests: XCTestCase {
67+
/// var contextProvider: ContextProvider<MyEnvironment>
68+
/// ...
69+
/// override func setUp() {
70+
/// contextProvider.setUp()
71+
/// ...
72+
/// }
73+
/// }
74+
/// ```
75+
public mutating func setUp() {
76+
eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
77+
eventLoop = eventLoopGroup.next()
78+
}
79+
80+
/// Shuts the event loop group down.
81+
///
82+
/// Call this in your test class's `.tearDown()` method:
83+
///
84+
/// ```swift
85+
/// final class MyHandlerTests: XCTestCase {
86+
/// var contextProvider: ContextProvider<MyEnvironment>
87+
/// ...
88+
/// override func tearDown() {
89+
/// XCTAssertNoThrow(try contextProvider.shutdown())
90+
/// ...
91+
/// }
92+
/// }
93+
/// ```
94+
public mutating func shutdown() throws {
95+
defer {
96+
eventLoop = nil
97+
eventLoopGroup = nil
98+
}
99+
try eventLoopGroup.syncShutdownGracefully()
100+
}
101+
102+
/// Returns the mocked initialization context.
103+
public func makeInitializationContext() -> MockInitializationContext<EnvironmentVariable> {
104+
.init(
105+
logger: logger,
106+
eventLoop: eventLoop,
107+
allocator: .init(),
108+
environmentValueProvider: environmentValueProvider)
109+
}
110+
111+
/// Returns the mocked runtime context.
112+
///
113+
/// - Parameter configuration: The configuration for the mocked runtime context.
114+
public func makeContext(
115+
configuration: MockContext<EnvironmentVariable>.Configuration = .init()
116+
) -> MockContext<EnvironmentVariable> {
117+
.init(
118+
eventLoop: eventLoop,
119+
configuration: configuration,
120+
environmentValueProvider: environmentValueProvider)
121+
}
122+
}
File renamed without changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// Dispatch+Utils.swift
3+
// LambdaExtras
4+
//
5+
// Created by Mathew Gacy on 1/19/24.
6+
//
7+
8+
import Dispatch
9+
10+
extension DispatchWallTime {
11+
/// The interval between the point and its reference point.
12+
var millisecondsSinceEpoch: Int64 {
13+
Int64(bitPattern: self.rawValue) / -1_000_000
14+
}
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// Logger+Utils.swift
3+
// LambdaExtras
4+
//
5+
// Created by Mathew Gacy on 1/7/24.
6+
//
7+
8+
import Foundation
9+
import Logging
10+
11+
public extension Logger {
12+
/// A logger for use in ``MockContext`` and ``MockInitializationContext``.
13+
static let mock = Logger(
14+
label: "mock-logger",
15+
factory: { _ in StreamLogHandler.standardOutput(label: "mock-logger") })
16+
}

Sources/LambdaMocks/MockContext.swift

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Logging
1212
import NIOCore
1313

1414
/// A mock function context for testing.
15-
public struct MockContext<E>: RuntimeContext, EnvironmentValueProvider {
15+
public struct MockContext<EnvironmentVariable>: RuntimeContext, EnvironmentValueProvider {
1616
public var requestID: String
1717
public var traceID: String
1818
public var invokedFunctionARN: String
@@ -23,8 +23,13 @@ public struct MockContext<E>: RuntimeContext, EnvironmentValueProvider {
2323
public var eventLoop: EventLoop
2424
public var allocator: ByteBufferAllocator
2525

26+
/// A closure returning a `TimeAmount` from a given `DispatchWallTime`.
27+
///
28+
/// This is used to return the remaining time until the context's ``deadline``.
29+
public var remainingTimeProvider: @Sendable (DispatchWallTime) -> TimeAmount
30+
2631
/// A closure returning the value of the given environment variable.
27-
public var environmentValueProvider: @Sendable (E) throws -> String
32+
public var environmentValueProvider: @Sendable (EnvironmentVariable) throws -> String
2833

2934
/// Creates a new instance.
3035
///
@@ -38,6 +43,7 @@ public struct MockContext<E>: RuntimeContext, EnvironmentValueProvider {
3843
/// - logger: The logger.
3944
/// - eventLoop: The event loop.
4045
/// - allocator: The byte buffer allocator.
46+
/// - remainingTimeProvider: A closure returning a `TimeAmount` from a given `DispatchWallTime`.
4147
/// - environmentValueProvider: A closure returning the value of the given environment
4248
/// variable.
4349
public init(
@@ -50,7 +56,8 @@ public struct MockContext<E>: RuntimeContext, EnvironmentValueProvider {
5056
logger: Logger,
5157
eventLoop: EventLoop,
5258
allocator: ByteBufferAllocator,
53-
environmentValueProvider: @escaping @Sendable (E) throws -> String
59+
remainingTimeProvider: @escaping @Sendable (DispatchWallTime) -> TimeAmount,
60+
environmentValueProvider: @escaping @Sendable (EnvironmentVariable) throws -> String
5461
) {
5562
self.requestID = requestID
5663
self.traceID = traceID
@@ -61,45 +68,104 @@ public struct MockContext<E>: RuntimeContext, EnvironmentValueProvider {
6168
self.logger = logger
6269
self.eventLoop = eventLoop
6370
self.allocator = allocator
71+
self.remainingTimeProvider = remainingTimeProvider
6472
self.environmentValueProvider = environmentValueProvider
6573
}
6674

67-
public func value(for environmentVariable: E) throws -> String {
75+
public func getRemainingTime() -> TimeAmount {
76+
remainingTimeProvider(deadline)
77+
}
78+
79+
public func value(for environmentVariable: EnvironmentVariable) throws -> String {
6880
try environmentValueProvider(environmentVariable)
6981
}
7082
}
7183

7284
public extension MockContext {
85+
86+
/// Configuration data for ``MockContext``.
87+
struct Configuration {
88+
/// The request ID, which identifies the request that triggered the function invocation.
89+
public var requestID: String
90+
91+
/// The AWS X-Ray tracing header.
92+
public var traceID: String
93+
94+
/// The ARN of the Lambda function, version, or alias that's specified in the invocation.
95+
public var invokedFunctionARN: String
96+
97+
/// The time interval before the context's deadline.
98+
public var timeout: DispatchTimeInterval
99+
100+
/// For invocations from the AWS Mobile SDK, data about the Amazon Cognito identity provider.
101+
public var cognitoIdentity: String?
102+
103+
/// For invocations from the AWS Mobile SDK, data about the client application and device.
104+
public var clientContext: String?
105+
106+
/// Creates an instance.
107+
///
108+
/// - Parameters:
109+
/// - requestID: The request ID.
110+
/// - traceID: The AWS X-Ray tracing header.
111+
/// - invokedFunctionARN: The ARN of the Lambda function.
112+
/// - timeout: The time interval before the context's deadline.
113+
/// - cognitoIdentity: Data about the Amazon Cognito identity provider.
114+
/// - clientContext: Data about the client application and device.
115+
public init(
116+
requestID: String = "\(DispatchTime.now().uptimeNanoseconds)",
117+
traceID: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1",
118+
invokedFunctionARN: String = "arn:aws:lambda:us-east-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime",
119+
timeout: DispatchTimeInterval = .seconds(5),
120+
cognitoIdentity: String? = nil,
121+
clientContext: String? = nil
122+
) {
123+
self.requestID = requestID
124+
self.traceID = traceID
125+
self.invokedFunctionARN = invokedFunctionARN
126+
self.timeout = timeout
127+
self.cognitoIdentity = cognitoIdentity
128+
self.clientContext = clientContext
129+
}
130+
}
131+
132+
/// Returns the time interval between a given point in time and the current time.
133+
///
134+
/// - Parameter deadline: The time with which to compare now.
135+
/// - Returns: The time interval between the given deadline and now.
136+
@Sendable
137+
static func timeAmountUntil(_ deadline: DispatchWallTime) -> TimeAmount {
138+
.milliseconds(deadline.millisecondsSinceEpoch - DispatchWallTime.now().millisecondsSinceEpoch)
139+
}
140+
73141
/// Creates a new instance.
74142
///
75143
/// - Parameters:
76-
/// - timeout: The time interval at which the function will time out.
77-
/// - requestID: The request ID.
78-
/// - traceID: The tracing header.
79-
/// - invokedFunctionARN: The ARN of the Lambda function.
80144
/// - eventLoop: The event loop.
145+
/// - configuration: The context configuration.
146+
/// - logger: The logger.
81147
/// - allocator: The byte buffer allocator.
148+
/// - remainingTimeProvider:
82149
/// - environmentValueProvider: A closure returning the value of the given environment
83150
/// variable.
84151
init(
85-
timeout: DispatchTimeInterval = .seconds(3),
86-
requestID: String = UUID().uuidString,
87-
traceID: String = "abc123",
88-
invokedFunctionARN: String = "aws:arn:",
89152
eventLoop: EventLoop,
153+
configuration: Configuration = .init(),
154+
logger: Logger = .mock,
90155
allocator: ByteBufferAllocator = .init(),
91-
environmentValueProvider: @escaping @Sendable (E) throws -> String
156+
remainingTimeProvider: @escaping @Sendable (DispatchWallTime) -> TimeAmount = Self.timeAmountUntil,
157+
environmentValueProvider: @escaping @Sendable (EnvironmentVariable) throws -> String
92158
) {
93-
self.requestID = requestID
94-
self.traceID = traceID
95-
self.invokedFunctionARN = invokedFunctionARN
96-
self.deadline = .now() + timeout
97-
self.logger = Logger(
98-
label: "mock-logger",
99-
factory: { _ in StreamLogHandler.standardOutput(label: "mock-logger") }
100-
)
159+
self.requestID = configuration.requestID
160+
self.traceID = configuration.traceID
161+
self.invokedFunctionARN = configuration.invokedFunctionARN
162+
self.deadline = .now() + configuration.timeout
163+
self.cognitoIdentity = configuration.cognitoIdentity
164+
self.clientContext = configuration.clientContext
165+
self.logger = logger
101166
self.eventLoop = eventLoop
102167
self.allocator = allocator
168+
self.remainingTimeProvider = remainingTimeProvider
103169
self.environmentValueProvider = environmentValueProvider
104170
}
105171
}

0 commit comments

Comments
 (0)