LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第42回 テスト上の空論

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 42 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)

テスト上の空論

あるサービスのアカウントには、いくつかの種類 (FREE, PREMIUM, ...) があり、以下のように定義されているとします。

enum class AccountType {
    FREE,
    PREMIUM,
    BUSINESS,
    ULTIMATE
}

ここで、アカウントの種類ごとに異なるアイコンを表示したいとしましょう。もし、AccountType が UI によってのみ使われる列挙型であるならば、アイコン画像をプロパティとして持たせてしまうのも一つの選択肢です。

enum class AccountType(val iconImage: IconImage) {
    FREE(Icons.FREE_ACCOUNT_TYPE),
    PREMIUM(Icons.PREMIUM_ACCOUNT_TYPE),
    BUSINESS(Icons.BUSINESS_ACCOUNT_TYPE),
    ULTIMATE(Icons.ULTIMATE_ACCOUNT_TYPE)
}

しかしこの方法は、あくまでも AccountType が特定のモジュールやレイヤからしか使われない場合に有効です。AccountType がより広い範囲で使われるのにもかかわらず、特定モジュールのための値をプロパティにしてしまうと、AccountType の肥大化を招いてしまいます。

そのようなときは、AccountType からアイコン画像を取得するための変換の仕組みを別途用意するという方法があります。以下のコードでは、その変換の実装として Map を使っています。

internal val ACCOUNT_TYPE_TO_ICON_MAP: Map<AccountType, IconImage> = mapOf(
    AccountType.FREE to Icons.FREE_ACCOUNT_TYPE,
    AccountType.PREMIUM to Icons.PREMIUM_ACCOUNT_TYPE,
    AccountType.BUSINESS to Icons.BUSINESS_ACCOUNT_TYPE,
    AccountType.ULTIMATE to Icons.ULTIMATE_ACCOUNT_TYPE
)

ここでもし、新しいアカウント種別を AccountType に加えた場合、ACCOUNT_TYPE_TO_ICON_MAP[accountType] が null を返すようになるかもしれません。前回のレポート 「アーキテクチャ」ただいま工事中 では、新しい要素を追加したときの実装忘れを防ぐために、ユニットテストを書くことも一つの手段であることを説明しました。以下のテストは、null を返すような要素があった場合に失敗するようになっています。

@Test
fun testAccountIconCompleteness() {
    for (type in AccountType.entries) {
        assertNotNull(
            ACCOUNT_TYPE_TO_ICON_MAP[type],
            "Unknown type: $type. Update `ACCOUNT_TYPE_TO_ICON_MAP` to ..."
        )
    }
}

この変換のコードやテストに対して、なにか改善すべき点はありますか?

早いぞ、明確だぞ、確実だぞ

テストで列挙や直和が網羅されていることを保証することは、良いプラクティスではあるのですが、必ずしも最善とは限りません。

ソフトウェアの検証には様々な段階があり、代表的なものとしては以下が挙げられます。

  • ビルド時・コンパイル時などの静的検証
  • ユニットテストなどのソフトウェアテスト
  • 実行時のアサーション、エラー

このリストの中で、上の項目がより早い段階で欠陥を検知でき、かつ、より確実です。AccountType の変換を実装する場合、Java (14 以降) や Kotlin といった言語では、テストを書く代わりに、switchwhen で列挙が網羅されていることを保証できます。

internal fun toAccountTypeIconImage(type: AccountType): IconImage = when (type) {
    AccountType.FREE -> Icons.FREE_ACCOUNT_TYPE
    AccountType.PREMIUM -> Icons.PREMIUM_ACCOUNT_TYPE
    AccountType.BUSINESS -> Icons.BUSINESS_ACCOUNT_TYPE
    AccountType.ULTIMATE -> Icons.ULTIMATE_ACCOUNT_TYPE
}

このように実装すると、変換の実装漏れがあった場合にコンパイルエラーが発生します。そのため、列挙が網羅されていることをテストを動かすまでもなく検証できます。IDE を使って開発しているならば、コーディング中にエラーを表示させることもできます。

もちろん、これはソフトウェアテストが不要ということを主張しているわけではありません。依然として、静的検証できないものに関しては、ユニットテストなどで検証する必要があります。まずは、何を保証したいのかを考え、その性質に合わせて、検証の方法を選択する必要があります。

一言まとめ

テストや実行時アサーションの代わりに、静的検証を使うことを選択肢に入れる。

キーワード: static analysis, test, runtime assertion

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧