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


この投稿はSwift Tweets(Swift Tweets 2018 Spring - connpass)での発表をまとめたものです。

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

ポイント:

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

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 XCTestXCTestCase といった公式フレームワークの面影は全くない。
それでも、 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 XCTestSuite if 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のようなテストフレームワークの難しいところは、

  • 元の XCTestXCTestCase には存在しないメソッド 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 →|→ XCTestCaseXCTestNSObject

SPM on macOS環境:
QuickSpec_QuickSpecBase →|→ XCTestCaseXCTestNSObject

SPM on Linux環境:
QuickSpec →|→ XCTestCaseXCTest

spec() の実行

テスト実行時(XCTest動作時)に必ず働くメソッドをオーバーライドし、その中で spec() を呼んでいる。

Xcode環境

NSObject 由来の + (void)initialize; を使用。
initialize はそのクラスが最初に使われる直前に1度だけ実行される。

SPM環境

macOSでもLinuxでも spec() 呼び出しの実装位置は同じだが、それを実行するきっかけは異なる。

SPM on macOSでは XCTestCaseclass 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 にする。
のだが、 NSInvocationtestInvocations はswift-corelibs-foundationやswift-corelibs-xctestに見当たらない……。Implementation Statusにもいない。そのため spec() と同様に手動で対処する必要がある。

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

まとめ

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

公式のメソッドではない spec() がどうして実行されるのか/ spec() 内に書いた describeit がどのようにしてテストに数えられて実行されるのか
→メソッドを継承するなどした中で 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