概要
AWS SDK for SwiftにTesting and debuggingというセクションがあり、モックを使用した自動テストの方法が記載されています。
また、上記のドキュメント内で紹介されているコードはGithub上に公開されているので、こちらを確認するとより理解が深まります。
上記のコンテンツを参考にしてモックを使ったテストを実装したのでその方法についてメモを兼ねて記述します。
詳細
モックを用意して自動テストを実装する際の基本的な流れは以下の通りです。
1. Protocolを作成し、Protocol内でモックを作成したい関数を指定する
2. 1で作成したProtocolに準拠したAmazon S3へのアクセスを実装するクラスと入力パラメータに基づいてモックされた結果を返却するクラスを作成する
3. モックを作成した関数を使用しているクラスの関数の設定を変更する
4. XCTestで自動テストのテストコードを記述する
上から順に説明していきます。
1.Protocolを作成し、Protocol内でモックを作成したい関数を指定する
Protocolを定義し、Protocol内でモックで使用する関数の定義を記述します。
Protocolはいわゆるインターフェースで、このプロトコルを使用したクラスではプロトコル内で定義された関数を実装する必要があります。
今回は以下のようなProtocolを実装します。
protocol S3SessionProtocol {
func listObjectsV2(input: ListObjectsV2Input) async throws -> ListObjectsV2Output
}
このS3SessionProtocol
というプロトコルを使用します。
仮にこのプロトコルを使用したクラスを作成する場合、Protocol内で定義されているlistObjectsV2
の定義がない場合、エラーになります。
なおlistObjectsV2
というのはこちらのリンク内に存在する"バケット内のオブジェクトの一覧表示"する処理を行うサンプルコード内で登場するメソッドです。
2. 1のProtocolに準拠したAmazon S3へのアクセスを実装するクラスと入力パラメータに基づいてモックされた結果を返却するクラスを作成する
1.で定義したProtocolを使用して、
- Amazon S3へのリクエストを実装するクラス(S3Session)
- 入力パラメータに基づいてモックのレスポンスを返却するクラス(MockS3Session)
を実装します。
public class S3Session: S3SessionProtocol {
let client: S3Client
let region: String
init(region: String = "ap-northeast-1") throws {
self.region = region
self.client = try S3Client(region: self.region)
}
public func listObjectsV2(input: ListObjectsV2Input) async throws -> ListObjectsV2Output {
return try await self.client.listObjectsV2(input: input)
}
}
public class MockS3Session: S3SessionProtocol {
var mockFiles: [String] = []
init() {
self.mockFiles = [
// json
"MockTest/S3_2024-02-06 19-01-32.json",
"MockTest/S3_2024-02-07 20-01-32.json",
"MockTest/S3_2024-02-08 21-01-32.json",
"MockTest/S3_2024-02-06 19-01-32.json",
"MockTest/S3_2024-02-07 20-01-32.json",
"MockTest/S3_2024-02-08 21-01-32.json",
"MockTest/S3_2024-02-06 19-01-32.jpg",
"MockTest/S3_2024-02-07 20-01-32.jpg",
"MockTest/S3_2024-02-08 21-01-32.jpg",
"MockTest/S3_2024-02-06 19-01-32.jpg",
"MockTest/S3_2024-02-07 20-01-32.jpg",
"MockTest/S3_2024-02-08 21-01-32.jpg",
"MockTest/S3_2024-02-06 19-01-32.mp4",
"MockTest/S3_2024-02-07 20-01-32.mp4",
"MockTest/S3_2024-02-08 21-01-32.mp4",
"MockTest/S3_2024-02-06 19-01-32.mp4",
"MockTest/S3_2024-02-07 20-01-32.mp4",
"MockTest/S3_2024-02-08 21-01-32.mp4"
]
}
// 与えられたprefixでmockFiles内にあるデータの前方一致検索を行った後、該当するデータを返却する
public func listObjectsV2(input: ListObjectsV2Input) async throws -> ListObjectsV2Output {
var filteredFiles: [String] = []
if let prefix = input.prefix {
filteredFiles = self.mockFiles.filter { $0.hasPrefix(prefix) }
} else {
filteredFiles = self.mockFiles
}
let output = ListObjectsV2Output(contents: filteredFiles.map { S3ClientTypes.Object(key: $0, lastModified: Date()) })
return output
}
それぞれのクラスがS3SessionProtocol
を使用しているため、それぞれのクラス内でlistObjectsV2
が定義されていることが確認できます。
S3Sessionではそのまま引数をS3Clientで定義されている関数にそのまま引数を渡している(元の関数を使用した時と同様の結果を返却するようにしている)のに対し、モックテスト用のクラスではクラス内に定義されている配列内から結果を返すようになっています。
3. モックを作成した関数を使用しているクラスの関数の設定を変更する
モックを作成した関数内を使用しているクラスは以下のようなものとします。
enum DataType: String {
case image = "jpg"
case json = "json"
case mp4 = "mp4"
}
class AWSManager{
var session: S3SessionProtocol
init(session: S3SessionProtocol) {
self.session = session
}
/// 指定されたバケットに含まれるファイルのリストを文字列の配列として返します。
///
/// - Parameter bucket: ファイルリストを取得するバケットの名前。
/// - Returns: バケット内に含まれる各ファイルの名前を示す`String`オブジェクトの配列。
func listBucketFiles(bucket: String, prefix: String? = nil, fileExtension: String? = nil)
async throws -> [String]
{
let input = ListObjectsV2Input(
bucket: bucket,
prefix: prefix
)
let output = try await session.listObjectsV2(input: input)
var fileNames: [String] = []
guard let objList = output.contents else {
return []
}
for obj in objList {
if let objName = obj.key {
// 拡張子が指定されている場合は、その拡張子を持つファイルのみを追加する
if let fileExtension = fileExtension, objName.hasSuffix(".\(fileExtension)") {
fileNames.append(objName)
}
// 拡張子が指定されていない場合は、すべてのファイルを追加する
else if fileExtension == nil {
fileNames.append(objName)
}
}
}
return fileNames
}
}
このクラスで説明する箇所は以下の部分です。
var session: S3SessionProtocol
init(session: S3SessionProtocol) {
self.session = session
}
この箇所ではクラスをインスタンス化するときに呼び出し元からsession
情報を渡すようにコードを記述しています。
このコード内ではS3にリクエストする場合にはS3Session
を、自動テストを行う場合にはMockS3Session
を呼び出し元から渡すことでその切り替えが行えるようにしています。
4. XCTestで自動テストのテストコードを記述する
ここまできたら後はXCTestでテストコードを記述するのみです。
import XCTest
import AWSS3
import Foundation
import ClientRuntime
@testable import "ProjectName"
final class AWSManagerTests: XCTestCase {
var bucket:String = ""
var prefix:String = ""
var session: MockS3Session!
var awsManager: AWSManager!
override func setUp() {
super.setUp()
session = MockS3Session()
self.awsManager = AWSManager(session: self.session)
self.prefix = "MockTest"
}
override func tearDown() {
awsManager = nil
super.tearDown()
}
/// listBucketFilesでMockS3SessionのmockFiles内を検索した結果、データが全件の場合のテストをする
func testFunctionalListBucketFilesSearchResultAll() async throws {
// Arrange:MockS3SessionのmockFiles全てのデータをsearchResultに追加する
let searchResult = [
"MockTest/S3_2024-02-06 19-01-32.json",
"MockTest/S3_2024-02-07 20-01-32.json",
"MockTest/S3_2024-02-08 21-01-32.json",
"MockTest/S3_2024-02-06 19-01-32.json",
"MockTest/S3_2024-02-07 20-01-32.json",
"MockTest/S3_2024-02-08 21-01-32.json",
"MockTest/S3_2024-02-06 19-01-32.jpg",
"MockTest/S3_2024-02-07 20-01-32.jpg",
"MockTest/S3_2024-02-08 21-01-32.jpg",
"MockTest/S3_2024-02-06 19-01-32.jpg",
"MockTest/S3_2024-02-07 20-01-32.jpg",
"MockTest/S3_2024-02-08 21-01-32.jpg",
"MockTest/S3_2024-02-06 19-01-32.mp4",
"MockTest/S3_2024-02-07 20-01-32.mp4",
"MockTest/S3_2024-02-08 21-01-32.mp4",
"MockTest/S3_2024-02-06 19-01-32.mp4",
"MockTest/S3_2024-02-07 20-01-32.mp4",
"MockTest/S3_2024-02-08 21-01-32.mp4"
]
// Act
await awsManager.listBucketFiles(bucket: bucket, prefix: prefix)
// Assert
XCTAssertEqual(awsManager.fileNameArray,searchResult)
}
/// listBucketFilesでMockS3SessionのmockFiles内を検索した結果、データが0件の場合のテストをする
func testFunctionalListBucketFilesSearchResultZero() async throws {
// Arrange:何も存在しない場合を想定した結果をsearchResultに代入する.部分一致
self.prefix = "TestMock/S3"
let searchResult:[String] = []
// Act
await awsManager.listBucketFiles(bucket: bucket, prefix: prefix)
// Assert
XCTAssertEqual(awsManager.fileNameArray,searchResult)
// Arrange:何も存在しない場合を想定した結果をsearchResultに代入する.後方一致の場合のテストのため、.jsonより前の文字列に対して検索を行う
self.prefix = "22-01-32"
// Act
await awsManager.listBucketFiles(bucket: bucket, prefix: prefix)
// Assert
XCTAssertEqual(awsManager.fileNameArray,searchResult)
}
/// listBucketFilesでMockS3Sessionに定義されているmockFilesの内、fileExtensionをそれぞれ指定した場合の返り値が正しく返却されることをテスト
func testBoundaryListBucketFilesWithFileExtension() async throws {
// Arrange:MockS3SessionのmockFilesの内、jsonの場合に該当するケースを定義する。
var searchResult = [
"MockTest/S3_2024-02-06 19-01-32.json",
"MockTest/S3_2024-02-07 20-01-32.json",
"MockTest/S3_2024-02-08 21-01-32.json",
"MockTest/S3_2024-02-06 19-01-32.json",
"MockTest/S3_2024-02-07 20-01-32.json",
"MockTest/S3_2024-02-08 21-01-32.json"
]
// Act
await awsManager.listBucketFiles(bucket: bucket, prefix: prefix,fileExtension:DataType.json.rawValue)
// Assert
XCTAssertEqual(awsManager.fileNameArray,searchResult)
// Arrange:MockS3SessionのmockFilesの内、拡張子がjpgの場合に該当するケースを定義する。
mockFiles = [
"MockTest/S3_2024-02-06 19-01-32.jpg",
"MockTest/S3_2024-02-07 20-01-32.jpg",
"MockTest/S3_2024-02-08 21-01-32.jpg",
"MockTest/S3_2024-02-06 19-01-32.jpg",
"MockTest/S3_2024-02-07 20-01-32.jpg",
"MockTest/S3_2024-02-08 21-01-32.jpg"
]
awsManager.fileNameArray = []
// Act
await awsManager.listBucketFiles(bucket: bucket, prefix: prefix,fileExtension:DataType.image.rawValue)
// Assert
XCTAssertEqual(awsManager.fileNameArray,searchResult)
// Arrange:MockS3SessionのmockFilesの内、拡張子がmp4の場合に該当するケースを定義する。
searchResult = [
"MockTest/S3_2024-02-06 19-01-32.mp4",
"MockTest/S3_2024-02-07 20-01-32.mp4",
"MockTest/S3_2024-02-08 21-01-32.mp4",
"MockTest/S3_2024-02-06 19-01-32.mp4",
"MockTest/S3_2024-02-07 20-01-32.mp4",
"MockTest/S3_2024-02-08 21-01-32.mp4"
]
awsManager.fileNameArray = []
// Act
await awsManager.listBucketFiles(bucket: bucket, prefix: prefix,fileExtension:DataType.mp4.rawValue)
// Assert
XCTAssertEqual(awsManager.fileNameArray,searchResult)
}
このファイルではサンプルとして
- listBucketFilesでMockS3SessionのmockFiles内を検索した結果、データが全件の場合のテスト
- listBucketFilesでMockS3SessionのmockFiles内を検索した結果、データが0件の場合のテスト
- listBucketFilesでMockS3Sessionに定義されているmockFilesの内、fileExtensionをそれぞれ指定した場合のテスト
を記述しています。
今回、初めてSwiftでS3のモックテストを実装しましたが、ドキュメントが充実している分、実装が容易でした。
単純なモックテストに限らず、他にも使い道がありそうなので探してみようと思います。