はじめに
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アノテーションは、以下のような場面で有用です:
- テストのために可視性を緩和する必要がある場合
- 可視性の緩和が一時的なものであることを明示したい場合
- プロダクションコードからの誤用を防ぎたい場合
ただし、以下の点も忘れないでください:
-
まずは設計を見直す -
@VisibleForTestingが必要ということは、設計を改善する余地があるかもしれません - publicインターフェースでテストできないか検討する - privateメソッドを直接テストするより、publicなAPIを通じてテストする方が良い場合が多いです
-
チーム全体で使用方針を統一する -
@VisibleForTestingを使う基準をチームで合意しておくと良いでしょう
@VisibleForTestingを適切に使うことで、テストしやすく、かつ保守しやすいコードを書くことができます。