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