XCTAssertの内部実装について

XCTAssertには内部実装の観点から3種類あってそれぞれ違いますという話です。

# アサーションとは何か

動的テストにおいて、実結果が期待結果と合うことを確認するやつ。テスト実装パターン「Fixture」「Test Case」「Check」「Test Suite」のうちのCheckにあたる。元祖SUnitでは `should:` で表記していた。

```smalltalk
SetTestCase>>testAdd
    empty add: 5.
    self should: [empty includes: 5]
```

JUnitでは `assert` `expected` `actual` と表現。第1引数と第2引数がそれぞれ区別されている(内部で違う処理がされているというわけではない)。

```java
public static void assertEquals(Object expected, Object actual)
```

XCTestでは `expression1` `expression2` と区別なく呼んでいる。 `@autoclosure` がついていて、例外も投げられるのがポイント。

```swift
func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> T, _ expression2: @autoclosure () throws -> T, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) where T : Equatable
```

# XCTestとは

OCUnitに取って代わったテストフレームワーク。[List of unit testing frameworks - Wikipedia](https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks)に記載があり、xUnitの一員とされている。

実装はたぶん3種類。

* Objective-C
* Swift2種
  * https://github.com/apple/swift-corelibs-xctest
  * https://github.com/apple/swift/tree/master/stdlib/public/SDK/XCTest

swift-corelibs-xctestには、README.mdに

> This version of XCTest uses the same API as the XCTest you are familiar with from Xcode. Our goal is to enable your project's tests to run on all Swift platforms without having to rewrite them.

とあるように、OSに関係するもの(UIやActivityなど)が入っていない。
一方でapple/swiftのほうには `// --- XCTest API Swiftification ---` とあったりOS X用の実装が見られたりする。

* apple/swiftのXCTest: Xcodeで作っている時に動く方。
* apple/swift-corelibs-xctest: `swift test` の時に動く方。

と考えてよさそう。
[Xcode - Features - Apple Developer](https://developer.apple.com/xcode/features/)の説明では

> XCTest APIs make it easy to build unit tests that exercise app functionality and are capable of running on Mac, iPad, iPhone, or Simulator.

ということになっている。

# XCTAssertとして何があるのか

カテゴリ別に

* Booleanアサーション
* Nilアサーション
* Equalityアサーション
* Comparableアサーション
* Errorアサーション(Swift版のみ)
* NSExceptionアサーション(ObjC版のみ)

がある。このほかにFailing Unconditionallyというカテゴリ( `XCTFail` )も存在する。

これらのアサーションが共通してやっていること:

1. `expression` という名前で与えられているクロージャを実行して値を得る
2. 期待通りの結果になっているか確認する( `XCTAssertTrue` なら1で得た値が `true` であることを確認する。 `XCTAssertEqual` なら2つの `expression` が同じかどうか確認する)
3. 期待通りだったら何もしない。1で例外が発生したり、2で期待通りの結果になっていないことがわかったりした時は、然るべきところに報告する

Kent Beck氏が提唱した理念によれば、テストフレームワークはfailure(期待結果と実結果が一致しない)とerror(failureより問題のある事態。確認していなかったこと)を区別する。swift-corelibs-xctestでもapple/swiftでもこの2つを区別して報告している。

# 本題: それぞれの違いについて

## apple/swift-corelibs-xctestとapple/swiftの違い

apple/swift-corelibs-xctestにはがっつり実装があるが、apple/swiftの方は

* CMakeLists.txt
* XCTest.swift
* XCTestCaseAdditions.mm

の3ファイルのみ。そのXCTest.swiftにも

* `XCTContext` と `XCUIElement` のextension
* アサーション各種

しかなく、 `XCTestCase` やアサーション内の一部の処理などの肝心なところが見当たらない。

アサーションにおける両者の違いとしては次の3つがある。

* 例外が発生した時の処理
* 例外スロー/失敗時の報告
* NaN比較

それ以外だと、apple/swiftの方にはひたすらtodoだのfixmeだの書いてあるとか。

### 例外が発生した時の処理

`expression` クロージャ実行時に起こりうる例外は、swift-corelibs-xctestでは `Swift.Error` のみ、apple/swiftではSwift由来の `Swift.Error` とObjC由来の `NSException` の2つになっている。

なので、swift-corelibs-xctestでは単にcatchしているだけ。

```swift
    let result: _XCTAssertionResult
    do {
        result = try expression()
    } catch {
        result = .unexpectedFailure(error)
    }
```

apple/swiftでは、 `blockErrorOptional: Error?` に `Swift.Error` を、 `exceptionResult` に `NSException` を入れることで両方を処理している。`_XCTRunThrowableBlockBridge` の実装が見当たらないので詳細はよくわからない。

```swift
  var blockErrorOptional: Error?

  let exceptionResult = _XCTRunThrowableBlockBridge({
    do {
      try block()
    } catch {
      blockErrorOptional = error
    }
  })

  if let blockError = blockErrorOptional {
    return .failedWithError(error: blockError)
  } else if let exceptionResult = exceptionResult {

    if exceptionResult["type"] == "objc" {
      return .failedWithException(
        className: exceptionResult["className"]!,
        name: exceptionResult["name"]!,
        reason: exceptionResult["reason"]!)
    } else {
      return .failedWithUnknownException
    }
  } else {
    return .success
  }
```

### 例外スロー/失敗時の報告

上のコードにもあるように、 `expression` の結果は一旦enumの形で保存される。その後、failure/errorであればその旨を報告してXCTAssertの処理が完了する。

#### 結果を表すenum

swift-corelibs-xctestのほうが「errorとfailureが区別される」というのがわかりやすい。

```swift
private enum _XCTAssertionResult {
    case success
    case expectedFailure(String?)
    case unexpectedFailure(Swift.Error)

    var isExpected: Bool {
        switch self {
        case .unexpectedFailure(_): return false
        default: return true
        }
    }

    以下略
}
```

`var isExpected: Bool` は `.success` 時と `.expectedFailure` 時に `true` になるもの。failureはあくまでありうる事態なので、 `isExpected` に含まれる。

一方、apple/swiftには `_XCTAssertionResult` のような「成功/error/failure」のenumはない。先ほどのコードで登場したenumは__expressionクロージャ実行の__「成功/ `Swift.Error` / `NSException` 」のenumである。

```swift
enum _XCTThrowableBlockResult {
  case success
  case failedWithError(error: Error)
  case failedWithException(className: String, name: String, reason: String)
  case failedWithUnknownException
}
```

そのため、 `_XCTThrowableBlockResult` で `.success` となってもそこで終わりではなく、期待結果との比較作業をしてから報告をする。

#### 報告処理

swift-corelibs-xctestでは、 `XCTestCase` にある `open func recordFailure(withDescription description: String, inFile filePath: String, atLine lineNumber: Int, expected: Bool)` メソッドを呼んだらアサーションの役目が終わる。
recordしたのちに行われるのは

* errorとfailureの数のカウント
* error/failureしたアサーションの位置を出力
* error/failureした数の出力

である。
数を記録しているのは `XCTestRun` で、

```swift
    /// The number of test executions recorded during the run.
    open private(set) var executionCount: Int = 0

    /// The number of test failures recorded during the run.
    open private(set) var failureCount: Int = 0

    /// The number of uncaught exceptions recorded during the run.
    open private(set) var unexpectedExceptionCount: Int = 0
```

というフィールドがあり、必要に応じてインクリメントされる。
出力を担っているのが `internal class PrintObserver` で、アサーションの位置や数えた合計を出力してくれる。

```swift
        printAndFlush("\(file):\(lineNumber): error: \(testCase.name) : \(description)")
```

```swift
        printAndFlush(
            "\t Executed \(testRun.executionCount) \(tests), " +
            "with \(testRun.totalFailureCount) \(failures) (\(testRun.unexpectedExceptionCount) unexpected) " +
            "in \(formatTimeInterval(testRun.testDuration)) (\(formatTimeInterval(testRun.totalDuration))) seconds"
        )
```

swift/appleでは `func _XCTRegisterFailure(_ expected: Bool, _ condition: String, _ message: @autoclosure () -> String, _ file: StaticString, _ line: UInt)` を呼ぶ。expectedはswift-corelibs-xctestの `isExpected` と意味が同じで、failureなら `true` 、error(例外スロー)なら `false` を入れる。conditionには `expression` や `Swift.Error` 、 `NSException` の詳細メッセージが入る。
具体的なregister処理や詳細メッセージの作り方はわからない。

### NaN比較

浮動小数点にはふつうの数以外にNaNというものも含まれている。

http://ieeexplore.ieee.org/document/4610935/

NaNはnot a number、非数を示す。 `.infinity` とはまた別。Swiftでは、 `.nan` を含む評価は `.isNaN` といった専用のものを除き全てfalseになる。 `.nan == .nan` ですらfalse。 `FloatingPoint` には `.nan` のサンプルコードが載っている。
https://developer.apple.com/documentation/swift/floatingpoint

比較評価でも__エラーなしで__falseを返すので、 `.nan` 専用の処理は必要ない。と思うのだが、apple/swift版では数値計算に入る前に `.nan` の判定が入っている。

```swift
return (!value1.isNaN && !value2.isNaN)
  && (abs(value1 - value2) <= accuracy)
```

swift-corelibs-xctest版では何の前置きもなしに比較を行なっている。

```swift
if abs(value1.distance(to: value2)) <= abs(accuracy.distance(to: T(0))) {
```

なお、 `-` と `distance` で違いはない。

> For two values x and y, the result of x.distance(to: y) is equal to y - x

## Swift版とObjC版の違い

### Equalityアサーション

ObjC版では「Cスカラ型」「 `id` 型」それぞれに対してアサーションが用意されている。説明書きに

スカラ型( `XCTAssertEqual` ):

> Generates a failure when expression1 != expression2.

`id` 型( `XCTAssertEqualObjects` ):

> Generates a failure when expression1 is not equal to expression2.

とあるように、この2つは比較方法が違う。これは同値比較の仕様から来たもの。同じような例としてはJava(プリミティブ型と参照型)がある。→[前に書いたQiita](https://qiita.com/S_Shimotori/items/adb0bf6653a0217667c4)

一方、Swift版には「 `T` 」「 `T?` 」の2種類が用意されている。

* apple/swift-corelibs-xctest版: https://github.com/apple/swift-corelibs-xctest/blob/0d488a365a92333a88906cdc62453ccaf0d0c7f8/Sources/XCTest/Public/XCTAssert.swift
* apple/swift版: https://github.com/apple/swift/blob/d03a575279cf5c523779ef68f8d7903f09ba901e/stdlib/public/SDK/XCTest/XCTest.swift

代表して行数が少ないswift-corelibs-xctest版の `XCTAssertEqual` を見てみる。apple/swift版もやっていることは同じ。
1つ目が `T` 、2つ目が `T?` 用。

```swift
let (value1, value2) = (try expression1(), try expression2())
if value1 == value2 {
    return .success
} else {
    return .expectedFailure("(\"\(value1)\") is not equal to (\"\(value2)\")")
}
```

```swift
let (value1, value2) = (try expression1(), try expression2())
if value1 == value2 {
    return .success
} else {
    return .expectedFailure("(\"\(String(describing: value1))\") is not equal to (\"\(String(describing: value2))\")")
}
```

比較ではなく、失敗後のメッセージの作成方法に違いがある。
Swiftにおける同値比較は `Equatable` が担っており、実際にどう比較するかは `==` が全部吸収してくれる。そのためObjCやJavaのように、比較方法別にアサーションを用意する必要はない。

メッセージの作り方で分けたのは、 `Optional<T>` をそのまま文字列補間するとWarningの対象になってしまうからと考えられる。ユーザさんの目にとまるところでやっちゃうと `Optional(Hello, world!)` と出て恥ずかしいやつ。

<img width="244" alt="Screen Shot 2017-10-25 at 0.42.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/84589/6cd36a17-f4d7-0e38-1e58-08d71ab3cb75.png">

このwarningの修正方法としては、 `a ?? "初期値"` などで `Optional<T>`
 自体を回避するか、 `String` の `init(describing:)` を使うことで"Optional()"と表示されても問題ないという意思表示をするかがあり、XCTAssertでは後者を採用している。

https://developer.apple.com/documentation/swift/string/2427941-init
https://github.com/apple/swift/blob/53b84192795aa007bbe4696699b80b2e243a05aa/stdlib/public/core/StringInterpolation.swift

### Compareアサーション

* ObjC版: Cスカラ型のみ
* Swift版: `T: Comparable` のみ

`XCTAssertEqual` で `T?` を含む同値判定ができたのは `Optional<T>` がきちんと `==` を多重定義しているからであって、 `Comparable` では `Optional<T>` を含む比較はできない。

だと思う。

### Error/NSExceptionアサーション

ObjCでは `XCTAssertThrows` `XCTAssertThrowsSpecific` `XCTAssertThrowsSpecificNamed` などと種類が多い一方で、Swiftでは `XCTAssertThrowsError` と `XCTAssertNoThrow` しかなく、エラーの種類までは見てくれない。

Swiftだとエラーの型の判断がむずいからかな…？
apple/swift版を見に行くとspecific系は全て `// FIXME: Unsupported` になっている。

https://github.com/apple/swift/blob/d03a575279cf5c523779ef68f8d7903f09ba901e/stdlib/public/SDK/XCTest/XCTest.swift

# まとめ

* XCTestは読みやすいし面白い
* FIXMEのところを実装できたらすごいなあ
* 「failure」「error」の違いは雰囲気で何となくやっていきましょう
    * Kent Beckさんの方が間違っているだけな気がする

# References

https://developer.apple.com/documentation/xctest/
https://github.com/apple/swift/tree/master/stdlib/public/SDK/XCTest
https://github.com/apple/swift-corelibs-xctest
http://junit.org/junit5/docs/current/api
https://web.archive.org/web/20150315073817/http://www.xprogramming.com/testfram.htm
https://www.iso.org/standard/63598.html