【Swift】テストについて学んでいく
What is this?
タイトルの通り、自分がテストについて無知無知なので、テストを勉強しながらメモを取っていくScrapって感じです。
あくまでメモなので、基本的に箇条書きになります。
参考文献を載せるので、興味のある方はぜひ、書籍の購入を検討してください
間違っているところや認識が違うてんがありましたらコメントください🙇♂️
参考文献
定義方法
- 名前のメソッドは
test
で始める - XCTestCaseクラスを継承したクラス内に記述する
- XCTestCaseクラスを利用するにはXCTestをimportする必要がある
import XCTest
final class SampleTests: XCTestCase {
func testExample() {
// ここにテストを書く
}
}
テスト対象をimport
- モジュール外からアクセスできるコードは
public
に限られる - 解決するには
@testable
属性を利用→internal
がpublic
と同様になる - Q.
private
も同様か?
import XCTest
@testable import Demo
final class SampleTests: XCTestCase {
func testExample() {
// ここにテストを書く
}
}
アサーションの種類
XCTestが提供するアサーションを利用する。用途はさまざまで、次の4つに分類される
- 単一の式を評価するアサーション
- 2つの式を比較するアサーション
- エラーの有無を評価するアサーション
- 無条件に失敗するアサーション
そもそも「アサーション」とは?
アサーションは、プログラムがある時点で満たしているべき条件を記述するための機能
条件が満たされていない場合はプログラムの実行を中断します。
単一の式を評価するアサーション
アサーション関数 | 式の型 | テストが成功する条件 |
---|---|---|
XCTAssert(_:)関数 | Bool | 引数の式がTrueを返す |
XCTAssertTrue(_:)関数 | Bool | 引数の式がTrueを返す |
XCTAssertFalse(_:)関数 | Bool | 引数の式がFalseを返す |
XCTAssertNil(_:)関数 | Optional<Wrapped>型 | 引数の式がnilを返す |
XCTAssertNotNil(_:)関数 | Optional<Wrapped>型 | 引数の式がnilでない値を返す |
final class SingleExpressionAssertionTests: XCTestCase {
func testExample() {
let value = 5
XCTAssert(value == 5) // 引数の式がTrueを返す時、テストが成功する
XCTAssertTrue(value > 0) // 引数の式がTrueを返す時、テストが成功する
XCTAssertFalse(value < 0) // 引数の式がfalseを返す時、テストが成功する
let nilValue = nil as Int?
XCTAssertNil(nilValue) // 引数の式がnilを返す時、テストが成功する
let optionalValue = Optional(1)
XCTAssertNotNil(optionalValue) // 引数の式がnilでない値を返す
}
}
XCTAssert(_:)
関数とXCTAssertTrue(_:)
関数の機能は同じ
→可読性に影響がでない場合はXCTAssert(_:)
関数を使うと良い
2つの式を比較するアサーション
アサーション関数 | 式の型 | テストが成功する条件 |
---|---|---|
XCTAssertEqual(_: _:)関数 | Equatableプロトコルに準拠した型 | 2つの結果が等しい |
XCTAssertEqual(_: _:accuracy:)関数 | FloatingPointプロトコルに準拠した型 | 2つの式の差分が第3引数で指定された値よりも小さい |
XCTAssertNotEqual(_: _:)関数 | Equatableプロトコルに準拠した型 | 2つの式の結果が等しくない |
XCTAssertLessThan(_: _:)関数 | Comparableプロトコルに準拠した型 | 第1引数 |
XCTAssertLessThanOrEqual(_: _:)関数 | Comparableプロトコルに準拠した型 | 第1引数 |
XCTAssertGreaterThan(_: _:)関数 | Comparableプロトコルに準拠した型 | 第1引数 |
XCTAssertGreaterThanOrEqual(_: _:)関数 | Comparableプロトコルに準拠した型 | 第1引数 |
import XCTest
final class TwoExpressionAssertionTests: XCTestCase {
func testExample() {
XCTAssertEqual("abc", "abc") // 2つの結果が等しい
XCTAssertEqual(0.002, 0.003, accuracy: 0.1) // 2つの差がaccuracyより小さい
XCTAssertNotEqual("abc", "def") // 2つの結果が異なる
XCTAssertLessThan(4, 7) // 第1引数 < 第2引数
XCTAssertLessThanOrEqual(7, 7) // 第1引数 ≦ 第2引数
XCTAssertGreaterThan(6, 4) // 第1引数 > 第2引数
XCTAssertGreaterThanOrEqual(4, 4) // 第1引数 ≧ 第2引数
}
}
エラーの有無を評価するアサーション
式がthrow
キーワードによるエラーを発生させるかどうかを評価
アサーション関数 | 式の型 | テストが成功する条件 |
---|---|---|
XCTAssertThrowsError(_:) | 任意 | エラーが発生した |
XCTAssertNoThrow(_:) | 任意 | エラーが発生しなかった |
final class ErrorAssertionTests: XCTestCase {
func test() {
XCTAssertThrowsError(
try throwableFunction(throwsError: true)
)
XCTAssertNoThrow(
try throwableFunction(throwsError: false)
)
}
}
↑
ここはもう少しわかりやすいサンプルコードがありそう
無条件に失敗するアサーション
-
XCTFail()
関数は無条件に失敗するアサーション関数 - テストが特定のパスを通らないことを保証したい場合などに使用する
以下のコードはInt?
型の定数optionalValue
に値が存在することを期待している。値が存在すると成功、値が存在しないと失敗
import XCTest
func test() {
let optionalValue = Optional(3)
guard let value = optionalValue else {
XCTFail()
return
}
// Valueを使ったテストを作成
}
↑
あまり使い所がわからない・・・
テストケース
- 個々のテストはXCTestCaseクラスを継承したクラスのメソッドとして定義される
- テストケース:同じクラスないに定義されたテストの集まり
- 関連するテストは同じテストケース内に配置する
テストの事前処理と事後処理
<例>
事前処理:テストの実行前にテスト用のデータを作成する
事後処理:作成したデータを削除する
共通の事前処理をsetUp()
メソッド、共通の事後処理をtearDown()
メソッドで定義する
setUP()
-
setUp()
メソッドには、インスタンスメソッドとクラスメソッドの両方がある - インスタンスメソッドはテストごとの事前処理を行う
- クラスメソッドはテストケース全体の事前処理を行う
import XCTest
final class SetUpTests: XCTestCase {
override class func setUp() {
super.setUp()
print("テストケース全体の事前処理") //1
}
override func setUp() {
super.setUp()
print("テストごとの事前処理") //2
}
func test1() {
print("テスト1") //3
}
func test2() {
print("テスト2") //4
}
}
-
setUp()
メソッドはテストケースに対して一度だけ実行される - インスタンスメソッドの
setUp()
メソッドは個々のテストごとに実行されているのがわかる
tearDown()メソッド
- 一時ファイルの削除など、テストの後に必要な処理を実行する
- インスタンスメソッドとクラスメソッドがある
- インスタンスメソッドでは、テストごとの事後処理を行う
- クラスメソッドでは、テストケース全体の事後処理を行う
import XCTest
final class TearDownTests: XCTestCase {
override class func tearDown() {
super.tearDown()
print("テストケース全体の事後処理")// 5
}
override func tearDown() {
super.tearDown()
print("テストごとの事後処理") // 2, 4
}
func test1() {
print("テスト1") // 1
}
func test2() {
print("テスト2") // 3
}
}
テストの実行のコントロール
- テストケースクラスにはテストの実行をコントロールする機能がある
- テストを中断、非同期処理を待ち合わせしたりすることができる
失敗時のテストの中断
- テストの途中でアサーションが失敗しても、後続の処理は実行される
- しかし、この挙動が問題になることがある
- 後続の処理が事前のアサーションの結果に依存している場合
- 実行時間に時間がかかる場合
例:Int!
型の定数optionalValue
の値がnil
であるため、XCTAssertNotNil(_:)
が失敗する。後続の処理は継続して実行されるため、optionalValue + 7
の評価時の実行時エラーが発生する
import XCTest
final class SomeTestCase: XCTestCase {
func testWithNil() {
let optionalValue: Int! = nil
XCTAssertNotNil(optionalValue)
XCTAssertEqual(optionalValue + 7, 10)
}
}
実行時にエラーが発生するとユニットテスト全体が停止してしまい、以降のテストが実行されない
これを避けるにはcontinueAfterFailure
プロパティにfalse
を指定する(trueにすると、test失敗しても最後まで処理する)
import XCTest
final class SomeTestCase: XCTestCase {
func testWithNil() {
+ continueAfterFailure = false
let optionalValue: Int! = nil
XCTAssertNotNil(optionalValue)
XCTAssertEqual(optionalValue + 7, 10)
}
}
エラーによるテストの中断
- テストメソッドに
throws
キーワードを付与すると、テストメソッドでエラーを発生させることができる - エラーが発生した時点でそのテストは中断され、失敗したとみなされる
final class FailByErrorTests: XCTestCase {
func testThrows() throws {
let int = try throwableIntFunction(throwsError: false)
XCTAssert(int > 0)
}
}
非同期処理の待ち合わせ
- テストは同期的に行われるため、非同期処理が含まれる場合は、その完了を待つ必要がある
例:非同期にセットされた値を後続のアサーションで利用する。非同期処理なため、テストは失敗する
import XCTest
@testable import Demo
final class GetIntAsyncTests: XCTestCase {
func testAsync() {
var optionalValue: Int?
getIntAsync { value in
optionalValue = value
}
XCTAssertEqual(optionalValue, 4)
}
}
- 非同期処理を待ち合わせするには、
expection(description:)
メソッドを使う- このメソッドは、非同期処理の結果を表現する
XCTestExpectation
クラスの値を返す - この値を引数にして、
wait(for: timeout:)
メソッドを呼び出す - これにより、その場所で
timeout
で指定された時間、非同期処理の待ち合わせを行うことを宣言する
- このメソッドは、非同期処理の結果を表現する
-
テスト失敗:
timeout
で指定した時間内にXCTestExpection
クラスの値に対してfullfill()
メソッドが実行されない場合
import XCTest
@testable import Demo
final class GetIntAsyncTests: XCTestCase {
func testAsync() {
+ let asyncExpectation = expectation(description: "aync")
var optionalValue: Int?
getIntAsync { value in
optionalValue = value
+ asyncExpectation.fulfill()
}
+ wait(for: [asyncExpectation], timeout: 3)
XCTAssertEqual(optionalValue, 4)
}
}
スタブ
スタブ:テスト対象の依存先を置き換え、テスト用の入力を与えるためのテクニック
スタブは主に外部システムとの連携部をテストする際に利用される
サーバとの通信を行う箇所に対するテスト内で実際に通信を行うことには、次のような問題がある
- ネットワークの状態にテスト結果が左右される
- サーバの状態にテスト結果が左右される
- 実行に時間がかかる
ユニットテストはアプリケーション単体の動作を保証するためのテスト
スタブを利用して外部システムからの入力を置き換える。
プロトコルによる実装の差し替え
- スタブにはさまざまな実現方法があるが、プロトコルを用いる方法が一般的
方法
- テスト対象に依存する型のプロパティやメソッドをプロトコルとして切り出す
- テスト対象を、依存する型とプロトコルを通じてやりとりするように書き換える
- これをすることにより、実際に依存する型を同じプロトコルに準拠した型と自由に置き換えることができる
例:URLSession
クラスを利用してHTTPステータスコードを取得する
class StatusFetcher {
private let urlSession: URLSession
init(urlSession: URLSession) {
self.urlSession = urlSession
}
func fetchStatus(of url: URL, completion: @escaping (Result<Int, Error>) -> Void) {
let task = urlSession.dataTask(with: url) { data, urlResponse, error in
switch (urlResponse, data, error) {
case (_, let urlResponse as HTTPURLResponse, _):
completion(.success(urlResponse.statusCode))
case (_, _, let error?):
completion(.failure(error))
default:
fatalError()
}
}
task.resume()
}
}
依存先のプロトコル化
URLSession
クラスが狙っている通信機能をスタブ化することを考える。
-
StatusFetcher
クラスが使っているURLSession
クラスの機能は、URL
型の値を使用して非同期的にData?
型とHTTPURLResponse?
型とError?
型の値を取得するもの- 通信が成功:
Data
型とHTTPURLResponse
型の値が存在する - 通信が失敗:
Error
型の値が存在する
- 通信が成功:
このメソッドを持つプロトコルをHTTPClient
プロトコルとして定義する
import Foundation
extension URLSession: HTTPClient {
public func fetchContents(of url: URL, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
let task = dataTask(with: url) { data, urlResponse, error in
switch (data, urlResponse, error) {
case (let data?, let urlResponse as HTTPURLResponse, _):
completion(.success((data, urlResponse)))
case (_, _, let error?):
completion(.failure(error))
default:
fatalError()
}
}
}
}
プロトコルに準拠したスタブの実装
- 通信を代用するためのスタブを定義する
- スタブは各メソッドが任意の値を返すように実装し、テストでの利用時に結果を自由にコントロールできるようにする
例:HTTPClient
プロトコルに準拠したStubHTTPClient
クラスを定義し、固定のResult<(Data, HTTPURLResponse)>
の値を返す実装
import Foundation
@testable import Demo
final class StubHTTPClient: HTTPClient {
let result: Result<(Data, HTTPURLResponse), Error>
init(result: Result<(Data, HTTPURLResponse), Error>) {
self.result = result
}
func fetchContents(of url: URL, completion: @escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [result] in
completion(result)
}
}
}
テストにおけるスタブの使用
スタブを用いて、「HTTPクライアントが200を返した時に、StatusFetcher
クラスが200を結果として返す」というテストを書く
import XCTest
@testable import Demo
final class StatusFetcherTests: XCTestCase {
let url = URL(string: "https://example.com")!
let data = Data()
func makeStubHTTPClient(statusCode: Int) -> StubHTTPClient {
let urlResponse = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
return StubHTTPClient(result: .success((data, urlResponse)))
}
func test200() {
let httpClient = makeStubHTTPClient(statusCode: 200)
let statusFetcher = StatusFetcher(httpClient: httpClient)
let responseExpectation = expectation(description: "waiting for response")
statusFetcher.fetchStatus(of: url) { result in
switch result {
case .success(let statusCode):
XCTAssertEqual(statusCode, 200)
case .failure:
XCTFail()
}
responseExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}
}
これで、StatusFetcher
クラスのテストを書くことができた