概要
APIクライアントのテストを書きます。
APIにはQiitaのAPIを使用します。
環境にはPlaygroundを使用しました。
環境
Xcode 13.1
手順
- APIクライアントを準備する
- テスト書く
APIクライアントを準備する
今回の記事の目的から逸れるため、詳しくは解説しません。
コードは以下です。
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)")
}
}
ユニットテストを記述する
コードは以下です。
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
が完了したことを通知します。
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()
全体のコードです。
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()
参考リンク