SwiftでAPIクライアントのユニットテストを記述する

## 概要
APIクライアントのテストを書きます。
APIにはQiitaのAPIを使用します。
環境にはPlaygroundを使用しました。

## 環境
`Xcode 13.1`

## 手順
1. APIクライアントを準備する
2. テスト書く

### APIクライアントを準備する
今回の記事の目的から逸れるため、詳しくは解説しません。
コードは以下です。

```swift:Playground
import Foundation

enum SessionError: Error {
    case noData
    case noResponse
    case unAcceptableStatusCode(Int)
}

enum HttpMethod: String {
    case `get` = "GET"
    case post = "POST"
}

enum Result<T> {
    case success(T)
    case failure(Error)
}

protocol Requestable {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var path: String { get }
    var httpMethod: HttpMethod { get }
}

final class Session {
    func send<T: Requestable>(_ request: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = request.baseURL.appendingPathComponent(request.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = request.httpMethod.rawValue
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let response = response as? HTTPURLResponse else {
                completion(.failure(SessionError.noResponse))
                return
            }
            
            guard 200..<300 ~= response.statusCode else {
                completion(.failure(SessionError.unAcceptableStatusCode(response.statusCode)))
                return
            }
            
            guard let data = data else {
                completion(.failure(SessionError.noData))
                return
            }
            
            do {
                let objects = try JSONDecoder().decode(T.Response.self, from: data)
                completion(.success(objects))
            } catch {
                completion(.failure(error))
            }
        }
        
        task.resume()
    }
}

struct Article: Codable {
    let title: String
}

struct ArticlesRequest: Requestable {
    typealias Response = [Article]
    var baseURL: URL = URL(string: "https://qiita.com")!
    var path: String = "/api/v2/items"
    var httpMethod: HttpMethod = .get
}

let session = Session()
let request = ArticlesRequest()

session.send(request) { result in
    switch result {
    case .success(let articles):
        print("[debug] \(articles.count)") // => [debug] 20
    case .failure(let error):
        print("[debug] \(error.localizedDescription)")
    }
}
```

### ユニットテストを記述する
コードは以下です。

```swift:Playground
import XCTest
...
final class PlaygroundTests: XCTestCase {
    func testGetArticles() {
        let session = Session()
        let request = ArticlesRequest()
        let expectation = expectation(description: "")
        
        session.send(request) { result in
            switch result {
            case .success(let articles):
                XCTAssertNotNil(articles.first)
                expectation.fulfill()
            case .failure(let error):
                print(error)
            }
        }
        
        wait(for: [expectation], timeout: 5)
    }
}
PlaygroundTests.defaultTestSuite.run()
```

出力は以下のようになると思います。

```
Test Case '-[__lldb_expr_13.PlaygroundTests testGetArticles]' started.
Test Case '-[__lldb_expr_13.PlaygroundTests testGetArticles]' passed (1.482 seconds).
Test Suite 'PlaygroundTests' passed at 2021-12-07 07:03:11.673.
	 Executed 1 test, with 0 failures (0 unexpected) in 1.482 (1.485) seconds
```

### 非同期テストの注意点
非同期処理のテストを書くときに注目すべきは `expectation` です。
`expectation`を使わずにテストを書いた場合、処理が完了する前にテストコードが終わってしまいます。
そのため、失敗して欲しいテストが成功してしまいます。

もう一度、テストコードを確認しましょう。

コードの最初に`expectation`を生成しています。
`testGetArticles()`の最後で`expectation`が完了するまで`wait`しています。(timeoutを5秒に設定しています。)

`XCTAssertNotNil(articles.first)`でレスポンスが取得できたことを確認できてから、
`expectation`を`fulfill`します。
これで`expectation`が完了したことを通知します。

```swift:Playground
final class PlaygroundTests: XCTestCase {
    func testGetArticles() {
        let session = Session()
        let request = ArticlesRequest()
        let expectation = expectation(description: "")
        
        session.send(request) { result in
            switch result {
            case .success(let articles):
                XCTAssertNotNil(articles.first)
                expectation.fulfill()
            case .failure(let error):
                print(error)
            }
        }
        
        wait(for: [expectation], timeout: 5)
    }
}
PlaygroundTests.defaultTestSuite.run()
```

全体のコードです。

```swift:Playground
import Foundation
import XCTest

enum SessionError: Error {
    case noData
    case noResponse
    case unAcceptableStatusCode(Int)
}

enum HttpMethod: String {
    case `get` = "GET"
    case post = "POST"
}

enum Result<T> {
    case success(T)
    case failure(Error)
}

protocol Requestable {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var path: String { get }
    var httpMethod: HttpMethod { get }
}

final class Session {
    func send<T: Requestable>(_ request: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = request.baseURL.appendingPathComponent(request.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = request.httpMethod.rawValue
        
        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let response = response as? HTTPURLResponse else {
                completion(.failure(SessionError.noResponse))
                return
            }
            
            guard 200..<300 ~= response.statusCode else {
                completion(.failure(SessionError.unAcceptableStatusCode(response.statusCode)))
                return
            }
            
            guard let data = data else {
                completion(.failure(SessionError.noData))
                return
            }
            
            do {
                let objects = try JSONDecoder().decode(T.Response.self, from: data)
                completion(.success(objects))
            } catch {
                completion(.failure(error))
            }
        }
        
        task.resume()
    }
}

struct Article: Codable {
    let title: String
}

struct ArticlesRequest: Requestable {
    typealias Response = [Article]
    var baseURL: URL = URL(string: "https://qiita.com")!
    var path: String = "/api/v2/items"
    var httpMethod: HttpMethod = .get
}

final class PlaygroundTests: XCTestCase {
    func testGetArticles() {
        let session = Session()
        let request = ArticlesRequest()
        let expectation = expectation(description: "")
        
        session.send(request) { result in
            switch result {
            case .success(let articles):
                XCTAssertNotNil(articles.first)
                expectation.fulfill()
            case .failure(let error):
                print(error)
            }
        }
        
        wait(for: [expectation], timeout: 5)
    }
}
PlaygroundTests.defaultTestSuite.run()

```

## 参考リンク

https://peaks.cc/books/iOS_architecture

https://hopita.hatenablog.com/entry/2018/08/30/233409