Quickはどのようにして働くのか #swtws

この投稿は[Swift Tweets](https://swift-tweets.github.io/)([Swift Tweets 2018 Spring - connpass](https://swift-tweets.connpass.com/event/77111/))での発表をまとめたものです。

テストフレームワークQuickがどのような仕組みで働くのか、説明してみようと思います。

ポイント：

* なぜQuickは公式のテストフレームワークではないのにXcodeやSPMにテストと認識されるのか
* 公式のメソッドではない `spec()` がどうして実行されるのか
* `spec()` 内に書いた `describe` や `it` がどのようにしてテストに数えられて実行されるのか

# Quickとは

https://github.com/Quick/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
// 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](https://developer.apple.com/documentation/xctest/xctestsuite)

> Only use `XCTestSuite` if you need to define your own custom test suites programmatically.

[Defining Test Cases and Test Methods | Apple Developer Documentation](https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods)

> 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](https://github.com/Quick/QuickOnLinuxExample)
LinuxMain.swift

```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](https://github.com/apple/swift-corelibs-foundation/blob/master/Docs/Status.md)にもいない。そのため `spec()` と同様に手動で対処する必要がある。

Quickに書いた `describe` や `it` は「 `_` でつなげる」「不適切な文字は `_` で置換する」ことでテスト名になる。

# まとめ

なぜQuickは公式のテストフレームワークではないのにXcodeやSPMにテストと認識されるのか
→XCTest.framework提供のクラスやメソッドを継承して使っているから

公式のメソッドではない `spec()` がどうして実行されるのか/ `spec()` 内に書いた `describe` や `it` がどのようにしてテストに数えられて実行されるのか
→メソッドを継承するなどした中で `spec()` などの必要な処理を実行してもらっているから。具体的にどこで実行してもらうかは環境による。

# 参考文献

[XCTest | Apple Developer Documentation](https://developer.apple.com/documentation/xctest)
[NSInvocation - Foundation | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/nsinvocation)
[initialize - NSObject | Apple Developer Documentation](https://developer.apple.com/documentation/objectivec/nsobject/1418639-initialize?language=objc)
[CharacterSet - Foundation | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/characterset)

[apple/swift-corelibs-foundation: The Foundation Project, providing core utilities, internationalization, and OS independence](https://github.com/apple/swift-corelibs-foundation)
[apple/swift-corelibs-xctest: The XCTest Project, A Swift core library for providing unit test support](https://github.com/apple/swift-corelibs-xctest)

[Quick/Quick: The Swift (and Objective-C) testing framework.](https://github.com/Quick/Quick)
[Quick/Nimble: A Matcher Framework for Swift and Objective-C](https://github.com/Quick/Nimble)