Moya
Swiftのネットワーク抽象化ライブラリ。Alamofireのラッパーとして、タイプセーフなAPI定義とネットワーク層の抽象化を提供。プロトコル指向設計により、テスト可能で保守性の高いネットワークレイヤーを実現。RxSwift、Combine統合サポート。
GitHub概要
スター15,298
ウォッチ245
フォーク2,009
作成日:2014年8月16日
言語:Swift
ライセンス:MIT License
トピックス
alamofirehacktoberfestnetworkingreactiveswiftrxswiftswift
スター履歴
データ取得日時: 2025/7/18 01:38
ライブラリ
Moya
概要
Moyaは「Swift向けの型安全で抽象化されたネットワークライブラリ」として開発された、iOSアプリ開発で最も人気のあるHTTPクライアントライブラリの一つです。「Network abstraction layer for Swift」をコンセプトに、複雑なネットワーク処理をシンプルで直感的なAPIで提供。TargetTypeプロトコルによる型安全なAPI定義、プラグインシステムによる拡張性、RxSwift・Combineとの完全統合により、Swift開発者にとって理想的なネットワーキングソリューションとして地位を確立しています。
詳細
Moya 2025年版はSwiftネットワーキングの決定版として10年以上の開発実績を持つ成熟したライブラリです。TargetTypeプロトコルベースの設計により、APIエンドポイントを型安全で再利用可能な方法で定義可能。プラグインアーキテクチャにより認証、ログ、キャッシュ、テストモック等の横断的機能を効率的に実装。RxSwift、ReactiveSwift、Combineとのネイティブ統合により、リアクティブプログラミングパターンを活用した高度な非同期処理をサポートします。テストフレンドリーな設計により、ユニットテストとUIテストの両方で優れた開発体験を提供。
主な特徴
- 型安全なAPI定義: TargetTypeプロトコルによるコンパイル時型チェック
- プラグインアーキテクチャ: 認証、ログ、キャッシュ機能の効率的な実装
- Reactiveエクステンション: RxSwift、Combine、ReactiveSwiftとの完全統合
- テストサポート: モック機能とテストターゲット専用の豊富なツール
- Alamofireベース: 業界標準ライブラリをベースとした信頼性の高い実装
- 包括的なドキュメント: 充実した公式ドキュメントとサンプルコード
メリット・デメリット
メリット
- TargetTypeプロトコルによる型安全性でランタイムエラーを大幅削減
- プラグインシステムによる横断的機能の統一的な実装と管理
- RxSwift/Combineとの統合により宣言的で読みやすい非同期コード記述
- 豊富なテスト支援機能により高品質なユニットテストとUIテストを実現
- Swiftコミュニティでの圧倒的な採用実績と豊富な学習リソース
- 継続的なアップデートによる最新Swift機能とiOSバージョンへの対応
デメリット
- 学習コストが比較的高く初心者には複雑に感じられる場合がある
- 小規模プロジェクトでは機能過多になりオーバーエンジニアリングのリスク
- プラグインやReactiveバインディングにより依存関係が複雑化する可能性
- Alamofireに依存するためサードパーティライブラリへの間接的依存
- 大規模なAPIでTargetType定義の管理が煩雑になる場合がある
- iOS以外のプラットフォーム(macOS、watchOS等)での対応に制限
参考ページ
書き方の例
インストールと基本セットアップ
// Package.swift での依存関係追加
dependencies: [
.package(url: "https://github.com/Moya/Moya.git", .upToNextMajor(from: "15.0.0"))
],
targets: [
.target(
name: "YourApp",
dependencies: [
"Moya",
// RxSwift使用の場合
.product(name: "RxMoya", package: "Moya"),
// Combine使用の場合
.product(name: "CombineMoya", package: "Moya"),
]
)
]
// CocoaPods での依存関係追加
// Podfile
pod 'Moya', '~> 15.0'
# RxSwift使用の場合
pod 'Moya/RxSwift', '~> 15.0'
# Combine使用の場合
pod 'Moya/Combine', '~> 15.0'
// Carthage での依存関係追加
// Cartfile
github "Moya/Moya" ~> 15.0
// Swift Package Manager (Xcode)
// File -> Add Package Dependencies...
// https://github.com/Moya/Moya.git
API定義とTargetTypeプロトコル実装
import Moya
import Foundation
// APIエンドポイントの定義
enum UserAPI {
case getUsers(page: Int, limit: Int)
case getUser(id: Int)
case createUser(name: String, email: String)
case updateUser(id: Int, name: String, email: String)
case deleteUser(id: Int)
case uploadAvatar(userId: Int, imageData: Data)
}
// TargetTypeプロトコルの実装
extension UserAPI: TargetType {
var baseURL: URL {
return URL(string: "https://api.example.com/v1")!
}
var path: String {
switch self {
case .getUsers:
return "/users"
case .getUser(let id), .updateUser(let id, _, _), .deleteUser(let id):
return "/users/\(id)"
case .createUser:
return "/users"
case .uploadAvatar(let userId, _):
return "/users/\(userId)/avatar"
}
}
var method: Moya.Method {
switch self {
case .getUsers, .getUser:
return .get
case .createUser:
return .post
case .updateUser:
return .put
case .deleteUser:
return .delete
case .uploadAvatar:
return .post
}
}
var sampleData: Data {
switch self {
case .getUsers:
return """
{
"users": [
{"id": 1, "name": "田中太郎", "email": "[email protected]"},
{"id": 2, "name": "佐藤花子", "email": "[email protected]"}
],
"pagination": {"page": 1, "total": 2}
}
""".data(using: .utf8)!
case .getUser:
return """
{"id": 1, "name": "田中太郎", "email": "[email protected]", "createdAt": "2025-01-01T00:00:00Z"}
""".data(using: .utf8)!
case .createUser, .updateUser:
return """
{"id": 1, "name": "田中太郎", "email": "[email protected]", "createdAt": "2025-01-01T00:00:00Z"}
""".data(using: .utf8)!
case .deleteUser:
return Data()
case .uploadAvatar:
return """
{"avatarUrl": "https://api.example.com/avatars/user123.jpg"}
""".data(using: .utf8)!
}
}
var task: Task {
switch self {
case .getUsers(let page, let limit):
return .requestParameters(
parameters: ["page": page, "limit": limit],
encoding: URLEncoding.queryString
)
case .getUser, .deleteUser:
return .requestPlain
case .createUser(let name, let email):
let parameters = ["name": name, "email": email]
return .requestParameters(parameters: parameters, encoding: JSONEncoding.default)
case .updateUser(_, let name, let email):
let parameters = ["name": name, "email": email]
return .requestParameters(parameters: parameters, encoding: JSONEncoding.default)
case .uploadAvatar(_, let imageData):
let formData = MultipartFormData(
provider: .data(imageData),
name: "avatar",
fileName: "avatar.jpg",
mimeType: "image/jpeg"
)
return .uploadMultipart([formData])
}
}
var headers: [String: String]? {
var headers = ["Content-Type": "application/json"]
// 認証トークンがある場合
if let token = AuthManager.shared.accessToken {
headers["Authorization"] = "Bearer \(token)"
}
return headers
}
var validationType: ValidationType {
return .successCodes
}
}
// データモデル定義
struct User: Codable {
let id: Int
let name: String
let email: String
let createdAt: Date?
enum CodingKeys: String, CodingKey {
case id, name, email
case createdAt = "created_at"
}
}
struct UsersResponse: Codable {
let users: [User]
let pagination: Pagination
}
struct Pagination: Codable {
let page: Int
let total: Int
let hasMore: Bool
enum CodingKeys: String, CodingKey {
case page, total
case hasMore = "has_more"
}
}
プロバイダーとリクエスト実行
import Moya
class UserService {
// プロバイダーの初期化
private let provider = MoyaProvider<UserAPI>(
endpointClosure: { target in
let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
return defaultEndpoint.adding(newHTTPHeaderFields: ["User-Agent": "MyApp/1.0"])
},
requestClosure: { (endpoint, done) in
do {
var request = try endpoint.urlRequest()
request.timeoutInterval = 30.0
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
},
plugins: [
NetworkLoggerPlugin(configuration: .init(logOptions: .verbose))
]
)
// ユーザー一覧取得
func getUsers(page: Int = 1, limit: Int = 20, completion: @escaping (Result<UsersResponse, Error>) -> Void) {
provider.request(.getUsers(page: page, limit: limit)) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let usersResponse = try decoder.decode(UsersResponse.self, from: response.data)
completion(.success(usersResponse))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// 特定ユーザー取得
func getUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
provider.request(.getUser(id: id)) { result in
switch result {
case .success(let response):
do {
// レスポンスステータスチェック
if response.statusCode == 200 {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: response.data)
completion(.success(user))
} else {
let error = NSError(domain: "UserServiceError", code: response.statusCode, userInfo: [
NSLocalizedDescriptionKey: "ユーザーの取得に失敗しました"
])
completion(.failure(error))
}
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// ユーザー作成
func createUser(name: String, email: String, completion: @escaping (Result<User, Error>) -> Void) {
provider.request(.createUser(name: name, email: email)) { result in
switch result {
case .success(let response):
do {
if response.statusCode == 201 {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: response.data)
completion(.success(user))
} else {
// エラーレスポンスの詳細処理
let errorResponse = try? JSONSerialization.jsonObject(with: response.data) as? [String: Any]
let message = errorResponse?["message"] as? String ?? "ユーザーの作成に失敗しました"
let error = NSError(domain: "UserServiceError", code: response.statusCode, userInfo: [
NSLocalizedDescriptionKey: message
])
completion(.failure(error))
}
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// ユーザー更新
func updateUser(id: Int, name: String, email: String, completion: @escaping (Result<User, Error>) -> Void) {
provider.request(.updateUser(id: id, name: name, email: email)) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let user = try decoder.decode(User.self, from: response.data)
completion(.success(user))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
// ユーザー削除
func deleteUser(id: Int, completion: @escaping (Result<Void, Error>) -> Void) {
provider.request(.deleteUser(id: id)) { result in
switch result {
case .success(let response):
if response.statusCode == 204 {
completion(.success(()))
} else {
let error = NSError(domain: "UserServiceError", code: response.statusCode, userInfo: [
NSLocalizedDescriptionKey: "ユーザーの削除に失敗しました"
])
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
// 使用例
class UserViewController: UIViewController {
private let userService = UserService()
override func viewDidLoad() {
super.viewDidLoad()
loadUsers()
}
private func loadUsers() {
userService.getUsers(page: 1, limit: 20) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let response):
print("ユーザー一覧取得成功: \(response.users.count)件")
// UIの更新処理
case .failure(let error):
print("エラー: \(error.localizedDescription)")
// エラーハンドリング
}
}
}
}
}
プラグインシステムによる拡張機能
import Moya
// 認証プラグイン
struct AuthPlugin: PluginType {
func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
var request = request
// トークンの自動追加
if let token = AuthManager.shared.accessToken {
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
// API キーの追加
request.addValue("your-api-key", forHTTPHeaderField: "X-API-Key")
return request
}
func willSend(_ request: RequestType, target: TargetType) {
print("🚀 リクエスト送信開始: \(target)")
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
print("✅ レスポンス受信成功: \(target) - Status: \(response.statusCode)")
case .failure(let error):
print("❌ リクエスト失敗: \(target) - Error: \(error)")
}
}
}
// キャッシュプラグイン
struct CachePlugin: PluginType {
private let cache = URLCache.shared
func willSend(_ request: RequestType, target: TargetType) {
// GETリクエストのみキャッシュ対象
guard let urlRequest = request.request,
urlRequest.httpMethod == "GET" else {
return
}
// キャッシュからレスポンスをチェック
if let cachedResponse = cache.cachedResponse(for: urlRequest) {
let age = Date().timeIntervalSince(cachedResponse.timestamp)
if age < 300 { // 5分以内のキャッシュは有効
print("📦 キャッシュからレスポンス取得: \(target)")
}
}
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
// 成功レスポンスをキャッシュに保存
if let urlRequest = response.request,
urlRequest.httpMethod == "GET",
response.statusCode == 200 {
let cachedResponse = CachedURLResponse(response: response.response!, data: response.data)
cache.storeCachedResponse(cachedResponse, for: urlRequest)
print("💾 レスポンスをキャッシュに保存: \(target)")
}
case .failure:
break
}
}
}
// ログプラグイン(カスタム)
struct CustomLoggerPlugin: PluginType {
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return formatter
}()
func willSend(_ request: RequestType, target: TargetType) {
let timestamp = dateFormatter.string(from: Date())
print("📤 [\(timestamp)] REQUEST: \(request.request?.httpMethod ?? "UNKNOWN") \(target)")
if let headers = request.request?.allHTTPHeaderFields {
print("📋 Headers: \(headers)")
}
if let body = request.request?.httpBody {
print("📝 Body: \(String(data: body, encoding: .utf8) ?? "Binary data")")
}
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
let timestamp = dateFormatter.string(from: Date())
switch result {
case .success(let response):
print("📥 [\(timestamp)] RESPONSE: \(response.statusCode) \(target)")
print("📋 Headers: \(response.response?.allHeaderFields ?? [:])")
if let responseString = String(data: response.data, encoding: .utf8) {
print("📄 Body: \(responseString)")
}
case .failure(let error):
print("🚨 [\(timestamp)] ERROR: \(error) \(target)")
}
}
}
// リトライプラグイン
struct RetryPlugin: PluginType {
private let maxRetries: Int
private let retryDelay: TimeInterval
init(maxRetries: Int = 3, retryDelay: TimeInterval = 1.0) {
self.maxRetries = maxRetries
self.retryDelay = retryDelay
}
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
// 500番台エラーの場合はリトライ対象
if response.statusCode >= 500 {
print("🔄 サーバーエラー検出、リトライ対象: \(response.statusCode)")
}
case .failure(let error):
// ネットワークエラーの場合はリトライ対象
if case .underlying(let nsError, _) = error,
(nsError as NSError).domain == NSURLErrorDomain {
print("🔄 ネットワークエラー検出、リトライ対象: \(nsError)")
}
}
}
}
// プラグイン統合プロバイダー
class EnhancedUserService {
private let provider = MoyaProvider<UserAPI>(
plugins: [
AuthPlugin(),
CachePlugin(),
CustomLoggerPlugin(),
RetryPlugin(),
NetworkLoggerPlugin(configuration: .init(
formatter: .init(responseData: { data in
return String(data: data, encoding: .utf8) ?? "Binary data"
}),
logOptions: [.requestBody, .responseBody]
))
]
)
func getUsersWithEnhancedFeatures(completion: @escaping (Result<UsersResponse, Error>) -> Void) {
provider.request(.getUsers(page: 1, limit: 20)) { result in
switch result {
case .success(let response):
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let usersResponse = try decoder.decode(UsersResponse.self, from: response.data)
completion(.success(usersResponse))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
RxSwiftとCombineによるReactive拡張
import Moya
import RxSwift
import RxCocoa
import Combine
// RxSwift版サービス
class RxUserService {
private let provider = MoyaProvider<UserAPI>()
private let disposeBag = DisposeBag()
// Observable を返すメソッド
func getUsers(page: Int = 1, limit: Int = 20) -> Observable<UsersResponse> {
return provider.rx
.request(.getUsers(page: page, limit: limit))
.filterSuccessfulStatusCodes()
.map(UsersResponse.self, using: JSONDecoder.iso8601)
.asObservable()
}
func getUser(id: Int) -> Observable<User> {
return provider.rx
.request(.getUser(id: id))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.asObservable()
}
func createUser(name: String, email: String) -> Observable<User> {
return provider.rx
.request(.createUser(name: name, email: email))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.asObservable()
}
// 複合操作例:ユーザー作成 → 詳細取得
func createAndFetchUser(name: String, email: String) -> Observable<User> {
return createUser(name: name, email: email)
.flatMapLatest { user in
self.getUser(id: user.id)
}
}
// エラーハンドリング付き
func getUsersWithErrorHandling(page: Int = 1) -> Observable<UsersResponse> {
return getUsers(page: page)
.retry(3)
.catchAndReturn(UsersResponse(users: [], pagination: Pagination(page: 1, total: 0, hasMore: false)))
}
}
// Combine版サービス
@available(iOS 13.0, *)
class CombineUserService {
private let provider = MoyaProvider<UserAPI>()
private var cancellables = Set<AnyCancellable>()
// Publisher を返すメソッド
func getUsers(page: Int = 1, limit: Int = 20) -> AnyPublisher<UsersResponse, Error> {
return provider.requestPublisher(.getUsers(page: page, limit: limit))
.filterSuccessfulStatusCodes()
.map(UsersResponse.self, using: JSONDecoder.iso8601)
.eraseToAnyPublisher()
}
func getUser(id: Int) -> AnyPublisher<User, Error> {
return provider.requestPublisher(.getUser(id: id))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.eraseToAnyPublisher()
}
func createUser(name: String, email: String) -> AnyPublisher<User, Error> {
return provider.requestPublisher(.createUser(name: name, email: email))
.filterSuccessfulStatusCodes()
.map(User.self, using: JSONDecoder.iso8601)
.eraseToAnyPublisher()
}
// 複合操作例:複数ユーザーの並列取得
func getMultipleUsers(ids: [Int]) -> AnyPublisher<[User], Error> {
let publishers = ids.map { getUser(id: $0) }
return Publishers.MergeMany(publishers)
.collect()
.eraseToAnyPublisher()
}
// リアルタイムデータ更新例
func startUserListUpdates() -> AnyPublisher<UsersResponse, Error> {
return Timer.publish(every: 30.0, on: .main, in: .common)
.autoconnect()
.flatMap { _ in
self.getUsers(page: 1, limit: 20)
}
.eraseToAnyPublisher()
}
}
// ViewModelでの使用例(RxSwift)
class RxUserListViewModel {
private let userService = RxUserService()
private let disposeBag = DisposeBag()
// 入力
let loadTrigger = PublishSubject<Void>()
let refreshTrigger = PublishSubject<Void>()
// 出力
let users = BehaviorRelay<[User]>(value: [])
let isLoading = BehaviorRelay<Bool>(value: false)
let error = PublishSubject<Error>()
init() {
let loadUsers = Observable.merge(loadTrigger, refreshTrigger)
.do(onNext: { [weak self] in
self?.isLoading.accept(true)
})
.flatMapLatest { [weak self] _ -> Observable<UsersResponse> in
guard let self = self else { return .empty() }
return self.userService.getUsers()
.catchError { error in
self.error.onNext(error)
return .empty()
}
}
.do(onNext: { [weak self] _ in
self?.isLoading.accept(false)
})
loadUsers
.map { $0.users }
.bind(to: users)
.disposed(by: disposeBag)
}
}
// ViewModelでの使用例(Combine)
@available(iOS 13.0, *)
class CombineUserListViewModel: ObservableObject {
private let userService = CombineUserService()
private var cancellables = Set<AnyCancellable>()
@Published var users: [User] = []
@Published var isLoading: Bool = false
@Published var errorMessage: String?
func loadUsers() {
isLoading = true
errorMessage = nil
userService.getUsers()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] response in
self?.users = response.users
}
)
.store(in: &cancellables)
}
}
// JSONDecoder拡張
extension JSONDecoder {
static let iso8601: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
テスト支援機能とモック実装
import Moya
import XCTest
// テスト用プロバイダー
class UserServiceTests: XCTestCase {
var sut: UserService!
var mockProvider: MoyaProvider<UserAPI>!
override func setUp() {
super.setUp()
// モックプロバイダーの設定
mockProvider = MoyaProvider<UserAPI>(
endpointClosure: { target in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(200, target.sampleData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
},
stubClosure: MoyaProvider.immediatelyStub
)
sut = UserService(provider: mockProvider)
}
override func tearDown() {
sut = nil
mockProvider = nil
super.tearDown()
}
// 成功ケースのテスト
func testGetUsersSuccess() {
let expectation = self.expectation(description: "Get users success")
sut.getUsers { result in
switch result {
case .success(let response):
XCTAssertEqual(response.users.count, 2)
XCTAssertEqual(response.users.first?.name, "田中太郎")
expectation.fulfill()
case .failure(let error):
XCTFail("Expected success, got error: \(error)")
}
}
waitForExpectations(timeout: 1.0)
}
// エラーケースのテスト
func testGetUsersNetworkError() {
// ネットワークエラーを模擬するプロバイダー
let errorProvider = MoyaProvider<UserAPI>(
endpointClosure: { target in
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkError(NSError(domain: "TestError", code: -1009, userInfo: nil)) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
},
stubClosure: MoyaProvider.immediatelyStub
)
let errorService = UserService(provider: errorProvider)
let expectation = self.expectation(description: "Get users network error")
errorService.getUsers { result in
switch result {
case .success:
XCTFail("Expected error, got success")
case .failure(let error):
XCTAssertNotNil(error)
expectation.fulfill()
}
}
waitForExpectations(timeout: 1.0)
}
// カスタムレスポンステスト
func testCreateUserWithCustomResponse() {
let customProvider = MoyaProvider<UserAPI>(
endpointClosure: { target in
let customData: Data
if case .createUser(let name, let email) = target {
let responseDict = [
"id": 999,
"name": name,
"email": email,
"created_at": "2025-01-01T12:00:00Z"
] as [String : Any]
customData = try! JSONSerialization.data(withJSONObject: responseDict)
} else {
customData = target.sampleData
}
return Endpoint(
url: URL(target: target).absoluteString,
sampleResponseClosure: { .networkResponse(201, customData) },
method: target.method,
task: target.task,
httpHeaderFields: target.headers
)
},
stubClosure: MoyaProvider.immediatelyStub
)
let customService = UserService(provider: customProvider)
let expectation = self.expectation(description: "Create user with custom response")
customService.createUser(name: "テストユーザー", email: "[email protected]") { result in
switch result {
case .success(let user):
XCTAssertEqual(user.id, 999)
XCTAssertEqual(user.name, "テストユーザー")
XCTAssertEqual(user.email, "[email protected]")
expectation.fulfill()
case .failure(let error):
XCTFail("Expected success, got error: \(error)")
}
}
waitForExpectations(timeout: 1.0)
}
}
// パフォーマンステスト
extension UserServiceTests {
func testGetUsersPerformance() {
self.measure {
let expectation = self.expectation(description: "Performance test")
sut.getUsers { _ in
expectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
}
}
// UserServiceの改良版(テスト対応)
class UserService {
private let provider: MoyaProvider<UserAPI>
init(provider: MoyaProvider<UserAPI>? = nil) {
self.provider = provider ?? MoyaProvider<UserAPI>()
}
// 実装は前のサンプルと同じ...
}