Skip to content

Commit

Permalink
Swift: Add mocking example
Browse files Browse the repository at this point in the history
This example demonstrates how to use a protocol to mock the AWS SDK for Swift, since Swift lacks the readwrite reflection required for traditional mocking. Passes the test (which is also the exemplar).

There is no build available on the web since this example is not one that corresponds to a particular function, but instead a technique for Swift developers using the SDK.

Once this PR is submitted, I'll start writing the AWS SDK for Swift Developer Guide chapter that will embed these snippets as it explains the mocking technique used here.
  • Loading branch information
shepazon authored and ford-at-aws committed Aug 21, 2023
1 parent 9b1f117 commit c5d5019
Show file tree
Hide file tree
Showing 6 changed files with 351 additions and 0 deletions.
43 changes: 43 additions & 0 deletions swift/example_code/swift-sdk/mocking/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to
// build this package.
//
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import PackageDescription

let package = Package(
name: "mocking",
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(
url: "https://github.com/awslabs/aws-sdk-swift",
from: "0.20.0"
)
],
// snippet-start:[mocking.swift.package.targets]
targets: [
// A target defines a module or a test suite. A target can depend on
// other targets in this package. They can also depend on products in
// other packages that this package depends on.
.executableTarget(
name: "mocking",
dependencies: [
.product(name: "AWSS3", package: "aws-sdk-swift"),
],
path: "./Sources"
),
// snippet-start:[mocking.swift.package.testTarget]
.testTarget(
name: "mocking-tests",
dependencies: [
.product(name: "AWSS3", package: "aws-sdk-swift"),
"mocking"
],
path: "./Tests"
)
// snippet-end:[mocking.swift.package.testTarget]
]
// snippet-end:[mocking.swift.package.targets]
)
49 changes: 49 additions & 0 deletions swift/example_code/swift-sdk/mocking/Sources/BucketManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// A class to manage Amazon Simple Storage Service (Amazon S3) operations
// using the ``S3Session`` class to access S3.
//

import Foundation
import ClientRuntime
import AWSS3

/// A class that uses an object that uses an object of type
/// ``S3SessionProtocol`` to access Amazon S3.
// snippet-start:[mocking.swift.using-session.class]
public class BucketManager {
/// The object based on the ``S3SessionProtocol`` protocol through which to
/// call SDK for swift functions. This may be either ``S3Session`` or
/// ``MockS3Session``.
var session: S3SessionProtocol

/// Initialize the ``S3Manager`` to call Amazon S3 functions using the
/// specified object that implements ``S3SessionProtocol``.
///
/// - Parameter session: The session object to use when calling Amazon S3.
// snippet-start:[mocking.swift.using-session.init]
init(session: S3SessionProtocol) {
self.session = session
}
// snippet-end:[mocking.swift.using-session.init]

/// Return an array listing all of the user's buckets by calling the
/// ``S3SessionProtocol`` function `listBuckets()`.
///
/// - Returns: An array of bucket name strings.
///
// snippet-start:[mocking.swift.using-session.calling]
public func getBucketNames() async throws -> [String] {
let output = try await session.listBuckets(input: ListBucketsInput())

guard let buckets = output.buckets else {
return []
}

return buckets.map { $0.name ?? "<unknown>" }
}
// snippet-end:[mocking.swift.using-session.calling]
}
// snippet-end:[mocking.swift.using-session.class]
56 changes: 56 additions & 0 deletions swift/example_code/swift-sdk/mocking/Sources/S3Session.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// A protocol and implementation to allow calling and mocking of the AWS SDK
// for Swift's `S3Client.listBuckets(input:)` function.
//

import Foundation
import ClientRuntime
import AWSS3

// snippet-start:[mocking.swift.protocol]
/// The S3SessionProtocol protocol describes the Amazon S3 functions this
/// program uses during an S3 session. It needs to be implemented once to call
/// through to the corresponding SDK for Swift functions, and a second time to
/// instead return mock results.
public protocol S3SessionProtocol {
func listBuckets(input: ListBucketsInput) async throws
-> ListBucketsOutputResponse
}
// snippet-end:[mocking.swift.protocol]

/// An implementation of ``S3SessionProtocol`` that calls the equivalent
/// functions in the AWS SDK for Swift. This class is used by the main program
/// instead of calling the SDK directly.
// snippet-start:[mocking.swift.session]
public class S3Session: S3SessionProtocol {
let client: S3Client
let region: String

/// Initialize the session to use the specified AWS Region.
///
/// - Parameter region: The AWS Region to use. Default is `us-east-1`.
init(region: String = "us-east-1") throws {
self.region = region

// Create an ``S3Client`` to use for AWS SDK for Swift calls.
self.client = try S3Client(region: self.region)
}

/// Call through to the ``S3Client`` function `listBuckets()`.
///
/// - Parameter input: The input to pass through to the SDK function
/// `listBuckets()`.
///
/// - Returns: A ``ListBucketsOutputResponse`` with the returned data.
///
// snippet-start:[mocking.swift.implement-real]
public func listBuckets(input: ListBucketsInput) async throws
-> ListBucketsOutputResponse {
return try await self.client.listBuckets(input: input)
}
// snippet-end:[mocking.swift.implement-real]
}
// snippet-end:[mocking.swift.session]
59 changes: 59 additions & 0 deletions swift/example_code/swift-sdk/mocking/Sources/mocking.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// An example demonstrating how to mock AWS SDK for Swift functions using
// protocols.
//

import Foundation
import ClientRuntime
import AWSS3

/// The main entry point for the example is an asynchronous main function.
@main
struct MockingDemo {
/// The static, asynchronous entry point for the program.
static func main() async {
// snippet-start:[mocking.swift.main-setup]
/// An ``S3Session`` object that passes calls through to the SDK for
/// Swift.
let session: S3Session
/// A ``BucketManager`` object that will be initialized to call the
/// SDK using the session.
let bucketMgr: BucketManager

// Create the ``S3Session`` and a ``BucketManager`` that calls the SDK
// using it.
do {
session = try S3Session(region: "us-east-1")
bucketMgr = BucketManager(session: session)
} catch {
print("Unable to initialize access to Amazon S3.")
return
}
// snippet-end:[mocking.swift.main-setup]

// snippet-start:[mocking.swift.main-call]
let bucketList: [String]

do {
bucketList = try await bucketMgr.getBucketNames()
} catch {
print("Unable to get the bucket list.")
return
}
// snippet-end:[mocking.swift.main-call]

// Print out a list of the bucket names.

if bucketList.count != 0 {
print("Found \(bucketList.count) buckets:")
for name in bucketList {
print(" \(name)")
}
} else {
print("No buckets found.")
}
}
}
95 changes: 95 additions & 0 deletions swift/example_code/swift-sdk/mocking/Tests/MockS3Session.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// An implementation of ``S3SessionProtocol`` that returns mock data instead
// of calling through to Amazon Web Services (AWS).

import Foundation
import ClientRuntime
import AWSS3

@testable import mocking

/// A structure format used to provide the data from which mock Amazon S3
/// buckets are built.
struct MockBucketInfo {
/// The bucket's name.
var name: String
/// The bucket's creation timestamp.
var created: Date
}

// snippet-start:[mocking.swift.mocksession]
/// The ``MockS3Session`` type implements ``S3SessionProtocol`` but instead of
/// calling through to the AWS SDK for Swift, its implementation of SDK
/// functions return mocked results.
public class MockS3Session: S3SessionProtocol {
/// An array of data used to construct the mock bucket descriptions.
private(set) var mockInfo: [MockBucketInfo]
/// An array of mock bucket descriptions as SDK `Bucket` objects.
var mockBuckets: [S3ClientTypes.Bucket] = []
/// A date formatter to convert ISO format date strings into timestamps.
let isoDateFormatter = ISO8601DateFormatter()

/// Initialize the mock session with some pretend buckets.
init() {
self.mockInfo = [
MockBucketInfo(
name: "swift",
created: isoDateFormatter.date(from: "2014-06-02T11:45:00-07:00")!
),
MockBucketInfo(
name: "amazon",
created: isoDateFormatter.date(from: "1995-07-16T08:00:00-07:00")!
),
MockBucketInfo(
name: "moon",
created: isoDateFormatter.date(from: "1969-07-20T13:17:39-07:00")!
)
]

// Construct an array of `S3ClientTypes.Bucket` objects containing the
// mock bucket data. The bucket objects only contain the minimum data
// needed to test against. Update this if additional bucket
// information is used by the main program.

for item in self.mockInfo {
let bucket = S3ClientTypes.Bucket(
creationDate: item.created,
name: item.name
)
self.mockBuckets.append(bucket)
}
}

/// Compare the specified names to the mock data and see if the names match.
///
/// - Parameter names: An array of bucket names to compare against the
/// expected names.
///
/// - Returns: `true` if the names match. `false` if they don't.
func checkBucketNames(names: [String]) -> Bool {
let sortedMockNames = (self.mockInfo.map { $0.name }).sorted()

return sortedMockNames == names.sorted()
}

// snippet-start:[mocking.swift.implement-mock]
/// An implementation of the Amazon S3 function `listBuckets()` that
/// returns the mock data instead of accessing AWS.
///
/// - Parameter input: The input to the `listBuckets()` function.
///
/// - Returns: A `ListBucketsOutputResponse` object containing the list of
/// buckets.
public func listBuckets(input: ListBucketsInput) async throws
-> ListBucketsOutputResponse {
let response = ListBucketsOutputResponse(
buckets: self.mockBuckets,
owner: nil
)
return response
}
// snippet-end:[mocking.swift.implement-mock]
}
// snippet-end:[mocking.swift.mocksession]
49 changes: 49 additions & 0 deletions swift/example_code/swift-sdk/mocking/Tests/mocking-tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Tests for the AWS SDK for Swift example. This demonstrates how to mock SDK
// functions.

import XCTest
import Foundation
import ClientRuntime
import AWSS3

@testable import mocking

// snippet-start:[mocking.swift.tests]
final class MockingTests: XCTestCase {
/// The session to use for Amazon S3 calls. In this case, it's a mock
/// implementation.
var session: MockS3Session? = nil
/// The ``BucketManager`` that uses the session to perform Amazon S3
/// operations.
var bucketMgr: BucketManager? = nil

/// Perform one-time initialization before executing any tests.
override class func setUp() {
super.setUp()
SDKLoggingSystem.initialize(logLevel: .error)
}

/// Set up things that need to be done just before each
/// individual test function is called.
override func setUp() {
super.setUp()

// snippet-start:[mocking.swift.tests-setup]
self.session = MockS3Session()
self.bucketMgr = BucketManager(session: self.session!)
// snippet-end:[mocking.swift.tests-setup]
}

// snippet-start:[mocking.swift.tests-call]
/// Test that `getBucketNames()` returns the expected results.
func testGetBucketNames() async throws {
let returnedNames = try await self.bucketMgr!.getBucketNames()
XCTAssertTrue(self.session!.checkBucketNames(names: returnedNames),
"Bucket names don't match")
}
// snippet-end:[mocking.swift.tests-call]
}
// snippet-end:[mocking.swift.tests]

0 comments on commit c5d5019

Please sign in to comment.