iOSアプリでちょっとした暗号化処理が必要な場合があります。たとえば何らかのハッシュ値をサーバーとの認証処理に使うといった例が考えられます。
ちょっとした処理なのでこんな感じで自前で実装することも多いのではないでしょうか?
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でユニットテストする方法について考えてみました。
以下では、僕のやってみた方法を書きましたが、もっと他に良い方法があれば教えていただければ幸いです。
ユニットテストの方針
自前の実装を「信頼できる既存の実装」と比べて結果が同じならばきちんと実装できていると考えて良いでしょう。
let hash = 自前実装
let expectedHash = 信頼できる既存の実装
XCTAssetEqual(hash, expectedHash)
というユニットテストを書きたいわけです。
幸い、macOSには openssl
コマンドが標準でインストールされています。下の例のように、opensslコマンドで大抵の暗号化処理を行うことができるので、これを「信頼できる既存の実装」としましょう。
$ printf 'hoge' | openssl dgst -binary -sha256 | openssl base64
7LZm13hyXslzBwRNZCv00WCqu3b1bABpxx6iWx6SaCU=
ユニットテストの実装
先ほどの方針のようにXCTestを書いていくことにします。
Step 1: Build Phaseでopensslコマンドを使ってテスト用データを用意
ユニットテストのBuild Phaseで任意のスクリプトを実行できます。ここから設定しましょう。
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ファイルができます。
enum HashTestSpec {
static var input: String { "foo" }
static var expected: String { "LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=" }
}
Step 2: XCTestで値を比較
あとはXCTAssertEqualで比較します。
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 が思い当たります。
CryptoSwiftのユニットテストは以下のようになっています。(見やすいように改行してあります。)
https://github.com/krzyzanowskim/CryptoSwift/blob/master/Tests/CryptoSwiftTests/DigestTests.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 から取得しているようです(ソースのヘッダ部分にリンクがあります)。
なるほど、信頼できるソースからコピーしてきたようです。これもありですね。