0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Android】@VisibleForTestingでテストしやすいコードを書く

Last updated at Posted at 2025-10-28

はじめに

Androidアプリ開発において、テストを書く際によく直面する問題があります。それは「privateなメソッドやプロパティをテストしたいけど、アクセスできない」という問題です。

単純にpublicにすれば解決しますが、それではクラスの設計が崩れてしまい、意図しない使われ方をされる可能性があります。

そこで登場するのが @VisibleForTesting アノテーションです。このアノテーションを使うことで、テストのために可視性を緩和しつつ、プロダクションコードからの不正なアクセスをLintで検出できます。

@VisibleForTestingとは

@VisibleForTestingは、AndroidXアノテーションライブラリが提供するアノテーションで、以下のような特徴があります。

  • テストのために可視性を緩和していることを明示的に示す
  • 本来の可視性レベルをotherwiseパラメータで指定できる
  • Lintがプロダクションコードからの不正なアクセスを警告してくれる

セットアップ

まず、プロジェクトに必要な依存関係を追加します。

dependencies {
    implementation "androidx.annotation:annotation:1.7.0"
}

基本的な使い方

シンプルな例

class UserRepository {
    
    // テストのためにinternalにしているが、本来はprivateであるべき
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal fun validateEmail(email: String): Boolean {
        return email.contains("@") && email.length > 5
    }
    
    fun registerUser(email: String, password: String): Result<User> {
        if (!validateEmail(email)) {
            return Result.failure(IllegalArgumentException("Invalid email"))
        }
        // ユーザー登録処理...
        return Result.success(User(email))
    }
}

この例では、validateEmailメソッドは本来privateであるべきですが、テストのためにinternalにしています。@VisibleForTestingアノテーションで、これがテスト目的であることを明示しています。

otherwiseパラメータの値

otherwiseパラメータには以下の値を指定できます。

  • VisibleForTesting.PRIVATE - 本来はprivateであるべき
  • VisibleForTesting.PACKAGE_PRIVATE - 本来はpackage-privateであるべき(Javaの互換性)
  • VisibleForTesting.PROTECTED - 本来はprotectedであるべき
  • VisibleForTesting.NONE - デフォルト値(本来の可視性を指定しない)

Kotlinでの実践的な使い方

プロパティへの適用

Kotlinでプロパティに@VisibleForTestingを使う場合、getter/setterのどちらに適用するかを明示する必要があります。

class ViewModel {
    
    // 読み取り専用プロパティの場合
    @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal val state: MutableStateFlow<UiState> = MutableStateFlow(UiState.Loading)
    
    // 読み書き可能なプロパティの場合
    @set:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal var isTestMode: Boolean = false
    
    fun loadData() {
        // データ読み込み処理
        state.value = UiState.Success(data)
    }
}

実際のユースケース例

Logger クラスでの使用例:

class Logger {
    companion object {
        @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
        var enabled = true
        
        fun d(tag: String, message: String) {
            if (enabled) {
                Log.d(tag, message)
            }
        }
        
        fun e(tag: String, message: String, throwable: Throwable? = null) {
            if (enabled) {
                Log.e(tag, message, throwable)
            }
        }
    }
}

テストコードでは:

class LoggerTest {
    
    @Test
    fun `ログが無効化されている場合、ログ出力されない`() {
        // テスト用にログを無効化
        Logger.enabled = false
        
        // ログ出力を試みる
        Logger.d("TEST", "This should not be logged")
        
        // 検証...
    }
    
    @After
    fun tearDown() {
        // テスト後に元に戻す
        Logger.enabled = true
    }
}

Lintによる検出

@VisibleForTestingの最大のメリットは、Lintがプロダクションコードからの不正なアクセスを検出してくれることです。

正しい使用例(テストコード内)

// test/java/com/example/UserRepositoryTest.kt
class UserRepositoryTest {
    
    @Test
    fun `無効なメルアドレスはバリデションエラー`() {
        val repository = UserRepository()
        
        // テストコードからのアクセスは問題なし
        assertFalse(repository.validateEmail("invalid"))
    }
}

警告が出る使用例(プロダクションコード内)

// main/java/com/example/MainActivity.kt
class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val repository = UserRepository()
        
        // ⚠️ Lint警告: "This method should only be accessed from tests"
        if (repository.validateEmail(email)) {
            // ...
        }
    }
}

Lintチェックを実行すると、以下のような警告が表示されます:

Warning: This method should only be accessed from tests or within private scope [VisibleForTests]

使用上の注意点とベストプラクティス

1. privateメソッドのテストは本当に必要か考える

@VisibleForTestingを使う前に、そもそもprivateメソッドを直接テストする必要があるか検討しましょう。多くの場合、publicなインターフェースを通じてテストできます。

// ❌ あまり良くない: privateメソッドを無理やりテスト
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun calculateTax(amount: Double): Double {
    return amount * 0.1
}

// ✅ より良い: publicメソッドを通じてテスト
fun getTotalPrice(amount: Double): Double {
    val tax = amount * 0.1
    return amount + tax
}

2. 設計の見直しシグナルとして捉える

@VisibleForTestingが必要になるケースは、設計を見直すシグナルかもしれません。

// もしかしたら、このクラスは責務が多すぎる?
class VendingMachine {
    
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal fun calculateTax(price: Double): Double { ... }
    
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal fun validatePayment(amount: Int): Boolean { ... }
    
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal fun checkInventory(itemId: String): Boolean { ... }
}

// 別クラスに分離することを検討
class TaxCalculator {
    fun calculate(price: Double): Double { ... }
}

class PaymentValidator {
    fun validate(amount: Int): Boolean { ... }
}

class InventoryChecker {
    fun check(itemId: String): Boolean { ... }
}

3. internal修飾子との併用

Kotlinではinternal修飾子と組み合わせることで、同一モジュール内からのみアクセス可能にできます。

class DataProcessor {
    
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal fun processData(input: String): String {
        return input.trim().uppercase()
    }
    
    fun execute(input: String): Result<String> {
        val processed = processData(input)
        // ...
        return Result.success(processed)
    }
}

4. ドキュメントとしての価値

@VisibleForTestingは、コードの意図を伝える優れたドキュメントにもなります。

class NetworkClient {
    
    // このプロパティがテスト目的で公開されていることが一目瞭然
    @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    internal val requestQueue: MutableList<Request> = mutableListOf()
    
    fun sendRequest(request: Request) {
        requestQueue.add(request)
        // リクエスト送信処理...
    }
}

CI/CDでのLintチェック

@VisibleForTestingの効果を最大化するために、CI/CDパイプラインでLintチェックを実行しましょう。

# .github/workflows/lint.yml
name: Lint Check

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          java-version: '17'
      - name: Run Lint
        run: ./gradlew lint
      - name: Upload Lint Report
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: lint-report
          path: app/build/reports/lint-results-*.html

まとめ

@VisibleForTestingアノテーションは、以下のような場面で有用です:

  • テストのために可視性を緩和する必要がある場合
  • 可視性の緩和が一時的なものであることを明示したい場合
  • プロダクションコードからの誤用を防ぎたい場合

ただし、以下の点も忘れないでください:

  1. まずは設計を見直す - @VisibleForTestingが必要ということは、設計を改善する余地があるかもしれません
  2. publicインターフェースでテストできないか検討する - privateメソッドを直接テストするより、publicなAPIを通じてテストする方が良い場合が多いです
  3. チーム全体で使用方針を統一する - @VisibleForTestingを使う基準をチームで合意しておくと良いでしょう

@VisibleForTestingを適切に使うことで、テストしやすく、かつ保守しやすいコードを書くことができます。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?