Swift: XCTContext.runActivity()の各階層で共通の事前/事後処理をまとめて書ける拡張
XCTest を書く際 setUp (setUpWithError)
/ tearDown (tearDownWithError)
を使えば、それぞれのテストケースの前後で任意の処理を実行できます。テストケースの事前処理や事後処理が共通している場合は、賢く綺麗にテストコードが書けます。また、XCTContext.runActivity()
を使うことで Spec 形式(describe / context / it)に近い形でテストコードを記述することが可能です。
ただ、Spec ではcontext
の層ごとにbeforeEach
やafterEach
を用いて共通の事前/事後処理をまとめて記述することができますが、XCTContext
の層ごとに共通の処理を記述する便利な API はありません。そこで、XCTContext
の層ごとにsetUp
とtearDown
を定義して実行できるようにする便利な拡張を考えました。
XCTContext を拡張
import XCTest
extension XCTContext {
typealias VoidClosure = () -> Void
typealias ThrowClosure = () throws -> Void
private static func runBlock(block: ThrowClosure) rethrows -> Void {
do {
try block()
} catch {
throw error
}
}
class func runActivities(
_ dummyClosure: ThrowClosure = {},
setUp: VoidClosure? = nil,
tearDown: VoidClosure? = nil,
_ blocks: ThrowClosure...
) rethrows {
try XCTContext.runBlock {
for block in blocks {
do {
setUp?()
try block()
tearDown?()
} catch {
throw error
}
}
}
}
}
XCTContext.runActivities(setUp:tearDown:blocks:)
というクラス関数を生やしました。
Gist にExtension のコードをアップロードしてあります。
これまでの書き方
これまでの書き方を説明するため、テスト対象の例として簡単な四則演算ができるクラスがあったとします。
import Foundation
enum MyError: Error {
case zeroDivided
}
public class MyMath {
let value: Double
init(value: Double) {
self.value = value
}
public func add(value: Double) -> Double {
return self.value + value
}
public func subtract(value: Double) -> Double {
return self.value - value
}
public func multiply(value: Double) -> Double {
return self.value * value
}
public func divide(value: Double) throws -> Double {
if value == 0 {
throw MyError.zeroDivided
}
return self.value / value
}
}
例えば、テスト対象(sut)のクラスの初期化とnil
リセットをそれぞれテストケースの前後で行いたい場合は下のようになると思います。
import XCTest
@testable import TestEach
class TestEachTests: XCTestCase {
func testMyMathLegacy() throws {
var sut: MyMath?
XCTContext.runActivity(named: "add 3+3=6") { _ in
sut = MyMath(value: 3) // sutの初期化
let actual = sut!.add(value: 3)
XCTAssertEqual(actual, 6)
sut = nil // sutをnilでリセット
}
XCTContext.runActivity(named: "subtract 3-3=0") { _ in
sut = MyMath(value: 3) // sutの初期化
let actual = sut!.subtract(value: 3)
XCTAssertEqual(actual, 0)
sut = nil // sutをnilでリセット
}
try XCTContext.runActivity(named: "divide 3/0 throw error") { _ in
sut = MyMath(value: 3) // sutの初期化
XCTAssertThrowsError(try sut!.divide(value: 0)) { error in
XCTAssertEqual(error as? MyError, MyError.zeroDivided)
}
sut = nil // sutをnilでリセット
}
}
}
XCTContext.runActivity()
の中で毎回 sut の初期化と nil リセットを記述しているのがイケてません。
拡張を導入した場合の書き方
XCTContext.runActivities()
の setUp クロージャーに共通の事前処理を tearDown クロージャーに共通の事後処理を書けば、blocks に渡した複数のクロージャーの処理前後でそれらが実行されます。blocks には }, { }
のように追記することで何個でもクロージャーを渡すことができます。クロージャーの中でXCTContext.runActivity()
を使ってもいいし、その中でコンテキストをネストしてもいいし、さらにXCTContext.runActivities()
を入れて使っても大丈夫です。
import XCTest
@testable import TestEach
class TestEachTests: XCTestCase {
func testMyMathNew() throws {
var sut: MyMath?
try XCTContext.runActivities(setUp: {
// 共通の事前処理
sut = MyMath(value: 3)
}, tearDown: {
// 共通の事後処理
sut = nil
}, {
// このクロージャーの前後でsetUp/tearDownが呼ばれる
XCTContext.runActivity(named: "add 3+5=8") { _ in
let actual = sut!.add(value: 5)
XCTAssertEqual(actual, 8)
}
}, {
// コンテキストを並列にすることもできる
XCTContext.runActivity(named: "subtract 3-2=1") { _ in
let actual = sut!.subtract(value: 2)
XCTAssertEqual(actual, 1)
}
XCTContext.runActivity(named: "subtract 3-2=1") { _ in
let actual = sut!.subtract(value: 2)
XCTAssertEqual(actual, 1)
}
}, {
// ErrorをThrowすることもできる
try XCTContext.runActivity(named: "multiply 3*4=12") { _ in
let sut = try XCTUnwrap(sut)
let actual = sut.multiply(value: 4)
XCTAssertEqual(actual, 12)
}
}, {
// コンテキストをネストすることもできる
try XCTContext.runActivity(named: "divide") { _ in
try XCTContext.runActivity(named: "divide 3/3=1") { _ in
let actual = try sut!.divide(value: 3)
XCTAssertEqual(actual, 1)
}
try XCTContext.runActivity(named: "divide 3/0 throw error") { _ in
XCTAssertThrowsError(try sut!.divide(value: 0)) { error in
XCTAssertEqual(error as? MyError, MyError.zeroDivided)
}
}
}
})
}
}
拡張のポイント
-
rethrows
を機能させるためにdummyClosure
という使用しないクロージャーを引数に定義 -
dummyClosure
の型についているthrows
が重要 - デフォルト引数として
{}
を設定してあることで省略できる
Discussion
XCTContext.runActivityで包むパターン