ユニットテストを書くときに、fixtureを読み込むなどの何らかの準備をすることがあります。そして、こういった処理もテスト対象の処理と同様に失敗する可能性があります。失敗を素直に表すならば、メソッドにthrowsをつけてエラーを投げられるようにします。

enum JSONFixture: String {
    struct FileNotFoundError: Error {}

    case foo
    case bar
    case baz

    func loadJSON() throws -> Any {
        let bundle = Bundle(for: TestBundleClass.self)
        guard let path = bundle.path(forResource: rawValue, ofType: "json") else {
            throw FileNotFoundError()
        }

        let url = URL(fileURLWithPath: path)
        let data = try Data(contentsOf: url)
        let json = try JSONSerialization.jsonObject(with: data, options: [])

        return json
    }
}

しかし、これではテストケースの中でtry, try?, try!のいずれかをつけなれけば、loadJSON()メソッドは実行できません。そもそも準備が成功しなければテストケースとして成立しないのに、エラーを考慮しなければならないのは不自然です。

「準備が成功しなければテストケースとして成立しない」という点に着目すれば、ここでのエラーはハンドルすべきものではないと捉えられます。そこで、エラーのスローの代わりにfatalError(_:)関数を使い、失敗時に実行を止め、外部にはエラーを伝えないようにします。

enum JSONFixture: String {
    case foo
    case bar
    case baz

    func loadJSON() -> Any {
        let optionalJSON = Bundle(for: TestBundleClass.self)
            .path(forResource: rawValue, ofType: "json")
            .flatMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) }
            .flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) }

        guard let json = optionalJSON else {
            fatalError("Could not load JSON file named \"\(rawValue)\"")
        }

        return json
    }
}

この場合、テストの実装時に準備の失敗を考慮する必要がないというメリットがあります。また、準備に失敗した場合もテストの実行が止まるので、確実に気づけます。

しかし、1つの準備の失敗でテスト全体が実行されなくなってしまうのは困る、というケースもあります。そこで、準備の失敗をテストの失敗として表すことを考えます。XCTFail(_:)関数などのXCTestの関数群は、テストに失敗した箇所を表す引数file, lineを持っています。loadJSON()メソッドにも同様の引数を追加し、XCTFail(_:)にそのまま渡せば、テストの失敗箇所をloadJSON()の実行箇所とできます。

enum JSONFixture: String {
    case foo
    case bar
    case baz

    func loadJSON(file: StaticString = #file, line: UInt = #line) -> Any {
        let optionalJSON = Bundle(for: TestBundleClass.self)
            .path(forResource: rawValue, ofType: "json")
            .flatMap { try? Data(contentsOf: URL(fileURLWithPath: $0)) }
            .flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) }

        guard let json = optionalJSON else {
            XCTFail("Could not load JSON file named \"\(rawValue)\"", file: file, line: line)
            return [:]
        }

        return json
    }
}

実際にこの準備を失敗させると、以下のスクリーンショットのようになります。

しかし、この方法にも欠点があり、失敗時に[:]という適当な値を返さなければなりません。

おわりに

準備の失敗時にテストの実行を止めるのが良いか、適当な値を返すのが良いかに応じて、適切な方法を選ぶと良いと思います。ちなみに、自分はこれまで主にfalalError(_:)関数で実行を止める方法を使っていましたが、XCTFail(_:)関数で扱うのも良いかもと思い始めたところです。