iOSで使う暗号化処理をユニットテストする方法を考える

iOSアプリでちょっとした暗号化処理が必要な場合があります。たとえば何らかのハッシュ値をサーバーとの認証処理に使うといった例が考えられます。
ちょっとした処理なのでこんな感じで自前で実装することも多いのではないでしょうか？

```swift
import Foundation
import CommonCrypto

final class MyCrypto {
    func sha256(_ string: String) -> String {
        var hash = [UInt8](repeating: 0,  count: Int(CC_SHA256_DIGEST_LENGTH))

        let data = string.data(using: .utf8)!
        data.withUnsafeBytes { pointer -> Void in
            CC_SHA256(pointer.baseAddress, CC_LONG(data.count), &hash)
        }
        return Data(hash).base64EncodedString()
    }
}
```

実装したはいいですが、ユニットテストしたいですよね？僕はしたいです。
この記事では上記のような処理をXCTestでユニットテストする方法について考えてみました。

以下では、僕のやってみた方法を書きましたが、もっと他に良い方法があれば教えていただければ幸いです。

## ユニットテストの方針

自前の実装を「信頼できる既存の実装」と比べて結果が同じならばきちんと実装できていると考えて良いでしょう。

```swift
let hash = 自前実装
let expectedHash = 信頼できる既存の実装

XCTAssetEqual(hash, expectedHash)
```

というユニットテストを書きたいわけです。

幸い、macOSには `openssl`コマンドが標準でインストールされています。下の例のように、opensslコマンドで大抵の暗号化処理を行うことができるので、これを「信頼できる既存の実装」としましょう。

```terminal:opensslコマンドでsha256
$ printf 'hoge' | openssl dgst -binary -sha256 | openssl base64
7LZm13hyXslzBwRNZCv00WCqu3b1bABpxx6iWx6SaCU=
```

### ユニットテストの実装

先ほどの方針のようにXCTestを書いていくことにします。

#### Step 1: Build Phaseでopensslコマンドを使ってテスト用データを用意

ユニットテストのBuild Phaseで任意のスクリプトを実行できます。ここから設定しましょう。

```shell
set -eu

swiftfilename="$SRCROOT/$TARGETNAME/HashTestSpec.generated.swift"

input='foo'
expected=$(printf "$input" | openssl dgst -binary -sha256 | openssl base64)

cat << EOF > "$swiftfilename"
enum HashTestSpec {
    static var input: String { "$input" }
    static var expected: String { "$expected" }
}
EOF
```

こうすると、以下のようなSwiftファイルができます。

```swift
enum HashTestSpec {
    static var input: String { "foo" }
    static var expected: String { "LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=" }
}
```

#### Step 2: XCTestで値を比較

あとはXCTAssertEqualで比較します。

```swift
class CryptoTests: XCTestCase {
    func testSHA256() {
        let input = HashTestSpec.input
        let hash = MyCrypto().sha256(input)
        let expected = HashTestSpec.expected
        XCTAssertEqual(hash, expected)
    }
}
```

#### サンプルプロジェクト

Xcode 11.3 でプロジェクトを開いてユニットテストを実行してみてください(Command + U)。
https://github.com/taka1068/CryptoUnitTestExample

### CryptoSwiftのテストを読む

以上が僕のたどりついたユニットテスト手法です。最後にサードパーティライブラリのユニットテストを読んでみます。
個人的に、著名ライブラリのユニットテストを読むと勉強になることが多いと思います。

Swift製の暗号化ライブラリといえば [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) が思い当たります。

CryptoSwiftのユニットテストは以下のようになっています。(見やすいように改行してあります。)
https://github.com/krzyzanowskim/CryptoSwift/blob/master/Tests/CryptoSwiftTests/DigestTests.swift

```swift
func testSHA3() {
    XCTAssertEqual(
        SHA3(variant: .sha224).calculate(for: Array<UInt8>(hex: "616263")).toHexString(), 
        "e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf"
    )
    XCTAssertEqual(
        SHA3(variant: .sha256).calculate(for: Array<UInt8>(hex: "616263")).toHexString(), 
        "3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532"
    )
    XCTAssertEqual(
        SHA3(variant: .sha384).calculate(for: Array<UInt8>(hex: "616263")).toHexString(),
        "ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25"
    )

    // ....
```

どうやら比較対象をユニットテストにベタ書きしているみたいです。この比較対象は、 http://www.di-mgt.com.au/sha_testvectors.html から取得しているようです(ソースのヘッダ部分にリンクがあります)。

なるほど、信頼できるソースからコピーしてきたようです。これもありですね。