XCTestでParameterizedTestを行う

こんにちは、id:numanuma08です。最近は業務でAndroidとiOSのコードを両方書いているので、「Androidだとできる○○をiOSでやるにはどうすれバインダー」(またはその逆)となるケースが多いです。今回もそんなネタから一つ。

ParameterizedTestを実行したい

jUnitはParameterized Testが実行可能です。

JUnit 5 User Guide

ある実装に対して入力や出力をリストで定義するテスト方法です。利用シーンとして、テキストのバリデーションで色々なパターンを試す場合やホワイトボックステストで境界値条件のテストなどがあります。ParameterizedTestを使うとテストに失敗したとき、どのパラメータで失敗したか表示されるのでそれを見て実装の修正やテストの修正ができます。

ではサンプルです。時事ネタと言うにはちょっと古いですが、軽減税率をテーマにします。enumで定義した製品と軽減税率かどうかのフラグをもとに、定価から税込価格を計算するメソッドをテストします。

// 製品一覧。プロパティに軽減税率対象かどうかのフラグを持っている
enum class Product(val reducedTax: Boolean) {
    Apple(reducedTax = true),
    Banana(reducedTax = true),
    NewsPaper(reducedTax = true),
    Beer(reducedTax = false),
    Pencil(reducedTax = false)
}

class ExampleUnitTest {

    // 小計を計算するメソッド
    private fun calc(product: Product, price: Int): Int {
        return if (product.reducedTax) {
            (price * 1.08).toInt()
        } else {
            (price * 1.1).toInt()
        }
    }

    @ParameterizedTest
    @ArgumentsSource(TestArgument::class)
    // ArgumentsSourceを使ってテスト用パラメータを受け取る
    fun calculate(product: Product, price: Int, expected: Int) {
        val actual = calc(product, price)
        assertEquals(expected, actual)
    }

    private class TestArgument : ArgumentsProvider {
        override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> =
            Stream.of(
                arguments(Product.Apple, 100, 108),
                arguments(Product.Banana, 100, 108),
                arguments(Product.NewsPaper, 100, 110),  // パラメータが間違っているので、ここでテストに失敗する
                arguments(Product.Beer, 100, 110),
                arguments(Product.Pencil, 100, 110)
            )
    }
}

このテストは失敗します。IntellijやAndroid Studioのコンソールにテスト結果が表示されます。

テストに失敗したパラメータが何だったのか分かりやすく表示されますね。この結果を見てテストのパラメータやテスト対象の機能を修正します。

Xcodeでも似たようなことをやりたい

残念ながらXCtestにParameterized Testに相当する機能はありません。しかし、XCTAssertEqualsはパラメータに行番号を取ります。行番号は#lineで取得できて、テストに失敗すると#lineで指定された行がハイライトされます。この仕組を使うとParameterized Testに近い実装が可能です。

Apple Developer Documentation

enum Product {
    case apple
    case banana
    case newsPaper
    case beer
    case pencil
    
    var reducedTax: Bool {
        switch self {
        case .apple:
            return true
        case .banana:
            return true
        case .newsPaper:
            return true
        case .beer:
            return false
        case .pencil:
            return false
        }
    }
}

class CalculateTest: XCTestCase {
    
    private func calculate(product: Product, price: Int) -> Int {
        if product.reducedTax {
            return Int(Double(price) * 1.08)
        } else {
            return Int(Double(price) * 1.1)
        }
    }
    
    func testCalculate() {
        let params: [(line: UInt, product: Product, price: Int, expected: Int)] = [
            (#line, .apple, 100, 108),
            (#line, .banana, 100, 108),
            // ここが間違っているので、テストに失敗する
            (#line, .newsPaper, 100, 110),
            (#line, .beer, 100, 110),
            (#line, .pencil, 100, 110),
        ]
        for p in params {
            let actual = calculate(product: p.product, price: p.price)
            XCTAssertEqual(p.expected, actual, line: p.line)
        }
    }
}

このテストを実行すると以下のように失敗したテストのパラメータがハイライトされます。

ハイライトされた箇所を見ながらテストのパラメータや実装を修正します。

まとめ

XCTestを使うときにjUnitのParameterizedTestにあたるものを実現する方法を示しました。いろいろなテスト手法を使って安全にアプリを開発したいですね。