この投稿はSwift Tweets(Swift Tweets 2018 Spring - connpass)での発表をまとめたものです。
テストフレームワークQuickがどのような仕組みで働くのか、説明してみようと思います。
ポイント:
- なぜQuickは公式のテストフレームワークではないのにXcodeやSPMにテストと認識されるのか
- 公式のメソッドではない
spec()がどうして実行されるのか -
spec()内に書いたdescribeやitがどのようにしてテストに数えられて実行されるのか
Quickとは
Quick is a behavior-driven development framework for Swift and Objective-C. Inspired by RSpec, Specta, and Ginkgo.
CocoaPodsでもCarthageでも動く。SPM on macOSでもon Linuxでも大丈夫。
各言語のドキュメントもある親切設計。
同じorganizationの中にNimbleがある。こちらはMatcherフレームワーク。
Quickが働く仕組み
以下のコードはREADME.mdに載っているサンプルコード。
import XCTest や XCTestCase といった公式フレームワークの面影は全くない。
それでも、 override func spec() に書けばちゃんとXcodeやSwift Package Managerがテストと認識してくれる。
// Swift
import Quick
import Nimble
class TableOfContentsSpec: QuickSpec {
override func spec() {
describe("the 'Documentation' directory") {
it("has everything you need to get started") {
let sections = Directory("Documentation").sections
expect(sections).to(contain("Organized Tests with Quick Examples and Example Groups"))
expect(sections).to(contain("Installing Quick"))
}
context("if it doesn't have what you're looking for") {
it("needs to be updated") {
let you = You(awesome: true)
expect{you.submittedAnIssue}.toEventually(beTruthy())
}
}
}
}
}
先に挙げた疑問の答えを一言で言うとズバリ、QuickがXCTest.frameworkをラップしているから。
そもそもXCTest.frameworkは、カスタマイズしたい人のために様々なクラスを public で提供するなどしている。
XCTestSuite - XCTest | Apple Developer Documentation
Only use
XCTestSuiteif you need to define your own custom test suites programmatically.
Defining Test Cases and Test Methods | Apple Developer Documentation
A test method is an instance method on an XCTestCase subclass, with no parameters, no return value, and a name that begins with the lowercase word test. Test methods are automatically detected by the XCTest framework in Xcode.
Quickのようなテストフレームワークの難しいところは、
- 元の
XCTestやXCTestCaseには存在しないメソッドspec()の中にテストを書いてもらう仕組みになっているので、何らかの方法でspec()を実行させなければならない - テストメソッドが
testナントカ()という名前ではない - XcodeプロジェクトだけでなくSPM on macOSやSPM on Linuxにも対応しなければならない
→ どのような環境でも実行してほしい処理を実行してもらいつつテストと認識してもらうような仕組みが必要
といったところ。に見える。
Quickが対応している環境
- Xcode環境(iOSやmacOSなどのアプリのプロジェクト)
- SPM on macOS環境
- SPM on Linux環境
それぞれで動いているFoundationやXCTest.frameworkは同一ではない。
環境毎に異なる実装をするための方法
Quickは3つのモジュールから構成されている。
- Quick ... 全てSwift。共通
- QuickObjectiveC ... 全てObjective C。Xcode環境のみ
- QuickSpecBase ... 全てObjective C。SPM on macOSのみ
Quick.podspecやPackage.swiftの中で使うパッケージとヘッダファイルが指定されている。
細かい分岐は
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) (Xcode環境時に真)
#if SWIFT_PACKAGE (SPM環境時に真)
#if SWIFT_PACKAGE && os(Linux) (SPM on Linux時に真)
で対応。
結果的に、3種の環境で QuickSpec の継承のようすが異なっている。
Xcode環境:
QuickSpec →|→ XCTestCase → XCTest → NSObject
SPM on macOS環境:
QuickSpec → _QuickSpecBase →|→ XCTestCase → XCTest → NSObject
SPM on Linux環境:
QuickSpec →|→ XCTestCase → XCTest
spec() の実行
テスト実行時(XCTest動作時)に必ず働くメソッドをオーバーライドし、その中で spec() を呼んでいる。
Xcode環境
NSObject 由来の + (void)initialize; を使用。
initialize はそのクラスが最初に使われる直前に1度だけ実行される。
SPM環境
macOSでもLinuxでも spec() 呼び出しの実装位置は同じだが、それを実行するきっかけは異なる。
SPM on macOSでは XCTestCase の class var defaultTestSuite: XCTestSuite { get } が呼ばれるついでに各種設定や spec() を実行してもらうようオーバーライドしている。
SPM on Linuxでは自動で実行するすべがないので、Quickは QCKMain という関数を提供している。Quickを使う人はLinuxMain.swiftで QCKMain を呼ばなければならない。
Quick/QuickOnLinuxExample: Testbed app for using Quick on Linux
LinuxMain.swift
QCKMain([
MySpec.self,
SampleLibrarySpec.self,
])
QCKMain の内部では各種設定と spec() の呼び出しを行なっている。その後、XCTest.frameworkの XCTMain を実行する。
テストメソッドの登録
基本的には
class var testInvocations: [NSInvocation]
@property(class, readonly, copy) NSArray<NSInvocation *> *testInvocations;
を用いればよい。 spec() からテストメソッドをかき集めて来て NSInvocation にする。
のだが、 NSInvocation や testInvocations はswift-corelibs-foundationやswift-corelibs-xctestに見当たらない……。Implementation Statusにもいない。そのため spec() と同様に手動で対処する必要がある。
Quickに書いた describe や it は「 _ でつなげる」「不適切な文字は _ で置換する」ことでテスト名になる。
まとめ
なぜQuickは公式のテストフレームワークではないのにXcodeやSPMにテストと認識されるのか
→XCTest.framework提供のクラスやメソッドを継承して使っているから
公式のメソッドではない spec() がどうして実行されるのか/ spec() 内に書いた describe や it がどのようにしてテストに数えられて実行されるのか
→メソッドを継承するなどした中で spec() などの必要な処理を実行してもらっているから。具体的にどこで実行してもらうかは環境による。
参考文献
XCTest | Apple Developer Documentation
NSInvocation - Foundation | Apple Developer Documentation
initialize - NSObject | Apple Developer Documentation
CharacterSet - Foundation | Apple Developer Documentation
apple/swift-corelibs-foundation: The Foundation Project, providing core utilities, internationalization, and OS independence
apple/swift-corelibs-xctest: The XCTest Project, A Swift core library for providing unit test support
Quick/Quick: The Swift (and Objective-C) testing framework.
Quick/Nimble: A Matcher Framework for Swift and Objective-C