クラウド Mac で検証する iOS 浮動小数点の境界値テスト
プログレスバーがときどき更新されない、グラフが突然空白になる。原因は UI スレッドではなく、上流から渡された NaN かもしれません。センサーの測定値を測定レンジで割る処理や、完了量を総量で割る処理では、不正な結果が生じることがあります。ArmShell のクラウド Mac では、まずこの計算部分を独立した Swift パッケージに切り出し、境界値テストで異常を検出してから iOS プロジェクトに組み込めます。
許容誤差を調整する前に、数値の契約を定義する
この記事で扱うのは、非負の測定値を 0 から 1 の範囲に正規化する処理だけです。入力は有限値、測定レンジは正の値でなければならず、結果も有限かつ範囲内である必要があります。不正なデータにはエラーを返し、自動的に範囲内へ丸め込むことはしません。測定値がゼロの場合は有効とし、負のゼロは正のゼロに統一して返します。
ここで見落としやすいのが、value < 0 || value > 1 では NaN を除外できない点です。NaN に対する順序比較はすべて false を返すため、そのまま有効な結果として処理が続いてしまう可能性があります。
有限値であること、業務上の範囲内であること、近似的に等しいことは、それぞれ別の検証です。テストの許容誤差を広げても、不正な入力は修正できません。
エラーの扱いも先に決めておく必要があります。画面に「データを利用できません」と表示するのは構いませんが、失敗を黙ってゼロに変換してはいけません。それでは、実際のゼロと読み取り失敗をユーザーが区別できなくなります。
シミュレータに依存しないテストパッケージを作成する
まずコマンドラインツールが使えることを確認し、一時ディレクトリにパッケージを作成します。以下の操作ではアプリのプロジェクトを変更せず、GUI セッションを起動する必要もありません。
xcrun swift --version
root="$(mktemp -d)"
cd "$root"
swift package init --type library --name NumericBoundary
Sources/NumericBoundary/NumericBoundary.swift の内容を次のコードに置き換えます。
public enum LevelError: Error, Equatable {
case invalidInput
case invalidResult
}
public func normalizedLevel(
_ raw: Double,
scale: Double
) throws -> Double {
guard raw.isFinite, scale.isFinite,
raw >= 0, scale > 0 else {
throw LevelError.invalidInput
}
let ratio = raw / scale
guard ratio.isFinite, ratio >= 0, ratio <= 1 else {
throw LevelError.invalidResult
}
return ratio == 0 ? 0.0 : ratio
}
入力の検証だけでは不十分です。有限値同士の除算でも、非常に大きな分子を非常に小さな正の分母で割ると、結果が無限大になることがあります。また、有限値であっても測定レンジを超える結果はエラーにするべきです。計算層の不備を UI コンポーネントに補わせてはいけません。
この実装は、通常の浮動小数点演算による丸めを許容します。業務上、最小有効測定値も必要なら、別途しきい値を設けてください。非常に小さな正の値を割り算すると、アンダーフローによってゼロになる場合があります。この例ではそれを許容しており、「有限であること」を「精度がまったく失われないこと」とは捉えていません。
反例で異常系を網羅する
既定のテストファイルの内容を、次のコードに置き換えます。各ケースではエラーの種類まで確認し、別の理由でエラーが投げられた場合に誤って成功と判定しないようにします。
import XCTest
@testable import NumericBoundary
final class NumericBoundaryTests: XCTestCase {
func testInvalidInputs() {
let cases: [(Double, Double)] = [
(.nan, 1),
(.infinity, 1),
(-.infinity, 1),
(1, .nan),
(1, .infinity),
(1, -.infinity),
(-1, 1),
(1, 0),
(1, -0.0),
(1, -1)
]
for (raw, scale) in cases {
XCTAssertThrowsError(
try normalizedLevel(raw, scale: scale)
) { error in
XCTAssertEqual(error as? LevelError, .invalidInput)
}
}
}
func testInvalidResults() {
let cases: [(Double, Double)] = [
(2, 1),
(.greatestFiniteMagnitude, .leastNormalMagnitude)
]
for (raw, scale) in cases {
XCTAssertThrowsError(
try normalizedLevel(raw, scale: scale)
) { error in
XCTAssertEqual(error as? LevelError, .invalidResult)
}
}
}
func testValidBoundaries() throws {
XCTAssertEqual(try normalizedLevel(0, scale: 1), 0)
XCTAssertEqual(try normalizedLevel(1, scale: 1), 1)
XCTAssertEqual(
try normalizedLevel(1, scale: 10),
0.1,
accuracy: 1e-12
)
let zero = try normalizedLevel(-0.0, scale: 1)
XCTAssertEqual(zero, 0)
XCTAssertEqual(zero.sign, .plus)
}
}
通常の等価比較では、負のゼロと正のゼロは等しいと判定されます。そのため、正規化の方針どおりに動作していることを確かめるには、sign も確認する必要があります。また、異常系で「結果が NaN と等しくない」というアサーションを使ってはいけません。NaN は自分自身とも等しくないため、そのテストでは何も証明できません。
許容誤差は業務上のスケールに合わせる
この例では出力範囲が 0 から 1 に固定されているため、絶対誤差による比較が分かりやすい選択です。1e-12 は、この小さな計算のテスト用に選んだ値であり、あらゆるセンサー、描画、統計処理に通用する精度基準ではありません。
計算値が広い範囲にわたる場合は、まず両方の値が有限であることを確認したうえで、業務要件に応じて絶対誤差と相対誤差を組み合わせます。
abs(actual - expected)
<= max(absoluteTolerance,
relativeTolerance * max(abs(actual), abs(expected)))
両方の許容誤差も、有限かつ非負である必要があります。値の桁が極端に大きい場合、差の計算や右辺の乗算がオーバーフローする可能性があるため、この式をそのまま汎用の安全なライブラリとして扱うことはできません。
ゼロ付近では主に絶対誤差が、大きな値の付近では主に相対誤差が判定を左右します。テストコードには、機器の分解能やアルゴリズムに割り当てた誤差予算など、しきい値の根拠を明記してください。テストが通るまで許容誤差を広げ続けるべきではありません。また、比較前に小数表記の文字列へ整形すると、実際の差を隠してしまうため避けます。
パイプラインに組み込み、品質ゲートの有効性を確かめる
パッケージのディレクトリで次を実行します。
swift test
swift test -c release
デバッグ構成とリリース構成の両方で実行してください。本番コードの検証には、最適化ビルドで削除される可能性のある assert ではなく、guard とエラーのスローを使います。パイプラインはコマンドの終了コードをそのまま受け取り、テストが失敗したら後続のデリバリー処理を停止しなければなりません。
続いて、意図的に検証処理を壊して確認します。入力チェックから raw.isFinite を一時的に取り除き、エラーの種類が変わることで NaN のケースが失敗することを確認してから、コードを元に戻します。これにより、単に関数が実行されたのではなく、入力時のチェックが実際にテストされていることを確かめられます。
アプリに組み込む際は、業務コードからも同じ実装を呼び出してください。テストパッケージ内に別のコピーを維持してはいけません。記録するのはツールチェーンのバージョン、コミット識別子、失敗したケースで十分であり、実際のユーザーの測定値をアップロードする必要はありません。最終的な合格基準は、不正な入力が明確に失敗し、有効な境界値が安定してテストを通過し、許容誤差に根拠があり、リリース構成でも同じ契約が適用されることです。
よくある質問
ゼロ未満または一より大きいかを調べれば NaN を除外できますか?
できません。NaN では両方の比較が false になります。先に isFinite を確認し、その後で許容範囲を検証します。
入力が有限値なら計算結果の検証は不要ですか?
必要です。有限値同士の除算でも無限大になる場合があります。結果の isFinite と業務上の許容範囲を再確認します。
次のビルドに備えてクラウドMacを準備
ワークロードに合わせて、マシンと利用期間をお選びください。Apple Silicon搭載の専有物理ノードを利用でき、仮想マシンではありません。
クラウドMacをレンタル