TECH PLAY

Android

イベント

マガジン

技術ブログ

はじめに KINTOテクノロジーズの大沼です。 モビリティサービス「my route」アプリの開発に従事しています。 本記事では、AndroidのKeystore、Cipher、DataStoreを使用して秘匿情報の暗号化と永続化を実装した際の実装詳細とハマった点・注意点をまとめました。 こちら大杉さんの記事 では、Tink を使用したケースとパフォーマンス検証を紹介しているのでぜひご一読ください。 💬 実装の前にディスカッション 🔍 本当に暗号化が必要なのか DroidKaigi 2025のyanzamさんのお話 でも触れられてましたが、Keystore がクラッシュするのは黙認しつつ最低限の機会頻度に抑えたいので、既存で暗号化しているデータが本当に暗号化する必要があるのかの議論をチームメンバーと交わしました。 案の定、不要に暗号化しているものもあり、議論することで最適なものに絞ることができました。 🏗️ アーキテクチャ セキュリティに関するリファクタリングのコードレビューは、心理負荷が高いと考えています。 私のチームはこういう時、大枠の実装方針を事前に共有し合うことで、コードレビュー時の認識違いや負担が減らせます。 今回、データの暗号化とインターフェースを以下のようなスライドで共有し、大きな齟齬なくレビューを進めることができました。 🛠️ 実装の流れ ここからは、実際の実装手順を以下の流れで解説します。 依存関係の追加 — DataStoreライブラリの導入 Keystoreを使った暗号化キーの生成 — AES/GCMの鍵をAndroid Keystoreで安全に管理 Cipherを使った暗号化・復号化 — 初期化ベクトル(IV)の扱いを含む暗号処理の実装 DataStoreへの保存 — 暗号化したデータをPreferences DataStoreで永続化し、Flowで読み出す 📚 依存関係の追加 ライブラリにDataStoreを追加します。 dependencies { // DataStore implementation("androidx.datastore:datastore-preferences:1.1.7") } 🔑 Keystoreを使った暗号化キーの生成 import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import java.security.KeyStore import javax.crypto.KeyGenerator ... fun getOrCreateSecretKey(): SecretKey? { try { // KeyStoreのインスタンス生成 val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER).apply { load(null) // KeyStoreを初期化するための必須の呼び出し } // KeyStoreにプロダクトの鍵が存在するか確認し、あれば取得し返す if (keyStore.containsAlias(PROJECT_KEY_STORE_ALIAS)) { val entry = keyStore.getEntry(PROJECT_KEY_STORE_ALIAS, null) if (entry is KeyStore.SecretKeyEntry) { return entry.secretKey } } // KeyStoreにプロダクトの鍵が存在しなければ生成して保存し返す val params = KeyGenParameterSpec.Builder( PROJECT_KEY_STORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, ) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setKeySize(256) .build() val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER, ) keyGenerator.init(params) return keyGenerator.generateKey() } catch (e: Exception) { Firebase.crashlytics.recordException(e) return null } } 🔐 Cipherを使った暗号化・復号化 import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase import java.util.Base64 import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec interface CryptographyManager { fun encrypt(plaintext: String): String fun decrypt(encryptedString: String): String } private const val TRANSFORMATION = "AES/GCM/NoPadding" private const val IV_SIZE_BYTES = 12 private const val TAG_SIZE_BITS = 128 class CryptographyManagerImpl : CryptographyManager { override fun encrypt(plaintext: String): String { return try { val cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey()) val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8)) val ivAndCiphertext = cipher.iv + ciphertext // IVと暗号文をバイト配列として結合 Base64.getEncoder().encodeToString(ivAndCiphertext) } catch (e: Exception) { try { CryptographyException.parse(e) } catch (cryptoException: CryptographyException) { Firebase.crashlytics.recordException(cryptoException) "" } } } override fun decrypt(encryptedString: String): String { return try { val cipher = Cipher.getInstance(TRANSFORMATION) val ivAndCiphertext = Base64.getDecoder().decode(encryptedString) // 復号化時に保存したIVを使う val spec = GCMParameterSpec(TAG_SIZE_BITS, ivAndCiphertext, 0, IV_SIZE_BYTES) cipher.init(Cipher.DECRYPT_MODE, getOrCreateSecretKey(), spec) val plaintext = cipher.doFinal( ivAndCiphertext, IV_SIZE_BYTES, ivAndCiphertext.size - IV_SIZE_BYTES, ) String(plaintext, Charsets.UTF_8) } catch (e: Exception) { try { CryptographyException.parse(e) } catch (cryptoException: CryptographyException) { Firebase.crashlytics.recordException(cryptoException) "" } } } } 💾 DataStoreへの保存 import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map data class SecureDataPreferences( val textData: String, ) object PreferencesKeys { private val TEXT_KEY = stringPreferencesKey("encrypted_text") } private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "encrypted_prefs") class SecureDataRepository( private val cryptographyManager: CryptographyManager ) { suspend fun saveTextData(data: String) { val encryptedData = cryptographyManager.encrypt(data) dataStore.edit { preferences -> preferences[TEXT_KEY] = encryptedData } } private val secureDataFlow: Flow<SecureDataPreferences> = secureDataStore.data .catch { exception -> if (exception is IOException) { emit(emptyPreferences()) } else { throw exception } } .map { it.mapSecureDataPreferences() } private fun Preferences.mapSecureDataPreferences(): SecureDataPreferences { return SecureDataPreferences( textData = this[PreferencesKeys.TEXT_KEY]?.let { cryptographyManager.decrypt(it) } ?: "", // ... Other data ) } suspend fun getTextData(): String { return try { withTimeout(3000L) { secureDataFlow.map { it.textData }.first { it.isNotBlank() } } } catch (_: TimeoutCancellationException) { "" } catch (_: NoSuchElementException) { "" } } } ⚠️ ハマった点・注意点 1. 初期化ベクトル(IV)の保存 暗号化時に生成されるIV(Initialization Vector)は、復号化時に必須です。 IVは暗号文と一緒に保存する必要があります。IVは秘密情報ではないため、平文で保存しても問題ありません。 ハマったポイント: 最初の実装でIVを保存し忘れ、復号化時に javax.crypto.AEADBadTagException が発生しました。 2. KeyStoreのキーのライフサイクル Android Keystoreに保存されたキーは、アプリをアンインストールすると削除されます。 また、デバイスのロック画面が解除されるまでキーにアクセスできない設定も可能です( setUserAuthenticationRequired(true) )。 注意点: keyが存在しない場合の処理を適切に実装する必要があります。 3. GCMモードのタグ長 GCM(Galois/Counter Mode)を使用する場合、タグ長を正しく設定する必要があります。 一般的には128ビット(16バイト)が使用されます。 4. エラーハンドリング 復号化時にはさまざまなエラーが発生する可能性があります: KeyPermanentlyInvalidatedException : キーが無効化された AEADBadTagException : 暗号文が改ざんされた、またはIVが間違っている InvalidKeyException : キーが無効 これらのエラーをハンドリングし、必要に応じてデータをクリアし再生成するなどの対応が必要です。 5. DataStoreの非同期処理 DataStoreはすべての操作が非同期で行われます。 CoroutineまたはFlowを使用して適切に処理する必要があります。 DataStoreのソースコード内で、最新の値を1つだけ取得できる data.first() を使用することを推奨しています。 // ViewModelでの使用例 viewModelScope.launch { repository.saveTextData(sensitiveData) } // Flowの監視 repository.secureDataFlow.map { it.textData }.first { it.isNotBlank() } 6. 無限待機の防止 DataStoreはディスクI/Oを伴う非同期処理です。first { it.isNotBlank() } は条件に一致する値が来るまで無限に待機しますが、 もしディスク読み込みが遅延したり、トークンが空のままだと、アプリがフリーズする可能性があり、タイムアウトを追加しました。 7. ProGuard/R8の設定 DataStore 1.1.5以降では、ProGuardルールがライブラリに内包されています。 巷の記事で ProGuardルール の記載が必要なことを目の当たりにしましたが、ルール記載なくリリースビルドしたところクラッシュせず、なぜ? となっていたところ、リリースノート確認し気づきました。 今回、1.1.7 を使用しているため、DataStore専用のProGuardルールを追加する必要はありません。 https://developer.android.com/jetpack/androidx/releases/datastore バージョン1.2.0-beta01で修正された問題として記載: "Fix java.lang.UnsatisfiedLinkError when using DataStore in an app which is optimized with R8" バージョン1.1.5で修正: "missing Proguard rules issue in the Android artifact of datastore-preferences-core" 8. 標準のSharedPreferencesMigrationが使えない EncryptedSharedPreferencesは特殊な暗号化を使用していて、 標準のマイグレーションでは暗号化されたままのデータが転送される また、EncryptedSharedPreferencesは 読み取り時に自動復号化されるのに対し、 今回は CryptographyManagerによる手動暗号化が必要です。 この部分を認知することができず、標準の標準のSharedPreferencesMigrationで実装し、テストしたところ復号できず判明しました。 マイグレーション時に適切な暗号化変換を実装しました。 まとめ 本記事では、Android Keystore、Cipher、DataStoreを組み合わせた秘匿情報の暗号化・永続化の実装について紹介しました。 実装前のディスカッションが重要 : そもそも暗号化が必要なデータかをチームで議論することで、Keystoreへのアクセス頻度を最小限に抑えられた Keystoreの鍵管理 : AES/GCMモードでの鍵生成とIV(初期化ベクトル)の保存を適切に行う必要がある DataStoreとの組み合わせ : Flowベースの非同期読み出しに対応するため、タイムアウトや無限待機の防止策が必要 EncryptedSharedPreferencesからの移行 : 標準のSharedPreferencesMigrationでは暗号化方式の違いにより復号できないため、手動でのマイグレーション実装が必要 Keystoreのクラッシュは完全には避けられませんが、暗号化対象を最適化し、エラーハンドリングを適切に実装することで、安定したセキュアなデータ管理を実現できました。 📣 追記: DataStore 1.3.0-alpha07 で暗号化サポートが追加 本記事の執筆後、 DataStore 1.3.0-alpha07 (2026年3月11日リリース)で、 Tinkライブラリを使用した暗号化サポート が新たに追加されました。 新しい androidx.datastore:datastore-tink アーティファクトにより、 AeadSerializer を使って既存のシリアライザをラップするだけで暗号化が実現できます。 val aeadSerializer = AeadSerializer( aead = keysetHandle.getPrimitive( RegistryConfiguration.get(), Aead::class.java, ), wrappedSerializer = ExistingSerializer, associatedData = "settings.json".encodeToByteArray(), ) 本記事で紹介したKeystore + Cipher による手動実装と比較すると、Tink統合によりボイラープレートが大幅に削減されます。ただし、現時点ではalpha版であるため、プロダクション導入の際はAPIの安定性を考慮する必要があります。今後のstable版リリースに注目です。 参考資料 Android Keystore System Jetpack DataStore 暗号化されたファイルの使用 Android セキュリティのベスト プラクティス DataStore 1.3.0-alpha07 リリースノート
こんにちは、LINEヤフー株式会社の福野です。社内のさまざまなアプリの開発を横断的に支援する仕事をしています。本記事では当社のAndroid・iOSアプリを衛星通信に対応させるための取り組みについてご...
アプリ開発の現場において、リリース後にユーザーから予期せぬ不具合報告が相次ぎ、対応に追われる経験はないでしょうか。 原因を振り返ると、テスト設計の不十分さや、ユーザー視点での検証不足に気づかされることも少なくありません。 アプリテストの本来の役割は、単にバグを見つけることだけではなく、プロダクトが提供すべき価値を保証し、ユーザー体験を最大化することにあります。 しかしWebとモバイルでの検証観点の違いや、膨大なテスト項目の優先順位付け、さらには自動化の判断基準など、実務レベルで品質を安定させるには多くの壁が存在します。 今回はアプリ開発のリーダー候補として品質改善に取り組むエンジニアに向けて、テストの種類や具体的な進め方、そして効率化のベストプラクティスを詳しく解説します。 属人化を排除し、チーム全体で高品質なアプリを継続的にリリースできる体制を構築するためのステップを一緒に見ていきましょう。 import haihaiInquiryFormClient from "https://form-gw.hm-f.jp/js/haihai.inquiry_form.client.js";haihaiInquiryFormClient.create({baseURL: "https://form-gw.hm-f.jp",formUUID: "927d2c4e-f06c-45b1-bd36-0240e55ccf72",}) ▼アプリ開発の基本について知りたい方はこちら▼ アプリ開発とは?種類・流れ・必要スキル・費用感まで初心者向けにわかりやすく解説 アプリテストとは?なぜ必要なのか アプリテストの目的は「不具合発見」だけではない アプリテストの主要な目的として、まず挙げられるのが品質保証です。 これは、単にプログラム上のバグを見つけることにとどまらず、プロダクトが仕様を満たし、本来提供すべき価値を正しく届けられているかを確認する一連のプロセスを指します。 開発の初期段階からテストを組み込むことで、リリース直前や運用開始後に重大な欠陥が発覚する事態を防ぎ、結果として修正コストの増大を抑制することができます。 また、ユーザー体験の向上も欠かせない視点です。 操作のしやすさや応答速度といったストレスのない利用環境を担保することで、ユーザーの離脱を防ぎ、信頼性の高いサービスとしての地位を確立できます。 さらに、多種多様な環境で正しく動作するかを確かめる互換性の確認や、外部攻撃から情報を守るためのセキュリティリスクの低減も、現代のアプリ開発においては必須の項目です。 不具合報告が相次ぐような状況を改善するには、これらの要素を網羅的に捉え、単なる動作確認を超えた品質管理の体制を構築することが重要となります。 Webアプリとモバイルアプリでテスト観点が変わる理由 開発対象がWebアプリかモバイルアプリかによって、重点を置くべきテスト観点は大きく異なります。 Webアプリの場合は、ChromeやSafariといったブラウザの種類やバージョンの違いによる挙動の変化が中心となりますが、モバイルアプリの場合は考慮すべき変数が飛躍的に増加します。 まずiOSとAndroidというOSの違いだけでなく、各メーカーが独自にカスタマイズした端末特有の依存関係が品質に強く影響します。 画面サイズや解像度のバリエーションも豊富なため、レイアウト崩れが起きていないかを詳細に確認しなければなりません。 さらに、モバイル特有の挙動であるプッシュ通知の受信、位置情報の利用許可、あるいは他のアプリへの切り替えに伴う中断と再開といったシナリオも検証の対象となります。 屋外での利用を想定した不安定な通信環境下での動作や、バッテリー消費の激しさなども、ユーザー満足度に直結する重要な要素です。 こうしたモバイルならではの物理的な制約や利用環境をシミュレートすることで、現場で発生しがちな「特定の条件下でだけ発生する不具合」を未然に防ぎ、再現性の高い品質基準を設けることが可能になります。 手動テストと自動テストの違い テストを効率化し、チーム全体の生産性を高めるためには、手動テストと自動テストの特性を理解して使い分けることが求められます。 手動テストは、人間が実際にアプリを操作して直感的に違和感を探る手法であり、特に新規機能のユーザーインターフェースや操作感の評価に向いています。 仕様が頻繁に変更される開発初期や、探索的な視点が必要な場面では、人の判断力による柔軟な対応が最大の武器となります。 一方で自動テストは、回帰テストのように何度も繰り返される定型的な検証において真価を発揮します。 プログラムによって高速かつ正確にテストを実行できるため、深夜や休日など時間を問わずに品質チェックを回し続けることが可能です。 これにより、人的ミスによる確認漏れを排除し、チームがより高度な設計や改善に注力できる環境を整えられます。 属人化を解消し、誰が実行しても同じ結果が得られる体制を構築するためには、まずは安定した機能から順次自動化を取り入れ、手動テストと組み合わせたハイブリッドな運用を推進するのがベストプラクティスです。 アプリテストの主な種類一覧 開発工程ごとのテストの種類 アプリ開発において品質を担保するためには、開発の流れに沿って段階的にテストを実施することが基本です。 まず行われるのが単体テスト(ユニットテスト)です。これはプログラムを構成する最小単位である関数やクラスが、設計通りに動作するかを確認する工程です。 不具合を早期に発見することで、後続工程での手戻りを最小限に抑える効果があります。 次に、個別のモジュールを組み合わせて正しくデータが受け渡されるかを検証するのが結合テスト(統合テスト)です。 複数の機能が連携した際に発生する矛盾や予期せぬ挙動を洗い出します。 さらに、アプリ全体を本番に近い環境で動かし、システム全体の要件を満たしているかを確認するのがシステムテスト(総合テスト)です。 ここでは性能や負荷といった側面も検証対象となります。 最後に、最終的な利用者が実際に操作を行い、ビジネス上の要件や使い勝手が満たされているかを判断するのが受け入れテスト(UAT)です。 エンジニア視点だけでなくユーザー目線での検証を徹底することで、リリース後の「期待していたものと違う」というミスマッチを防ぎます。 これらの工程を積み重ねることが、チーム全体の開発プロセスを標準化し、再現性の高い品質管理を実現するための第一歩となります。 観点別に見るテストの種類 機能が正しく動作するかを確認する機能テストは基本ですが、高品質なアプリをリリースするためには多角的な観点からの検証が欠かせません。 例えばUIテストでは、ボタンの配置や画面遷移が直感的に操作できるか、デザイン崩れがないかを確認します。 また、現代のアプリ開発において欠かせないAPIテストでは、バックエンドとの通信が正しく行われ、正確なデータが返却されるかを検証します。 さらに大量のアクセスがあった際に応答速度が低下しないかを見るパフォーマンステストや、脆弱性を突いた攻撃から情報を守るためのセキュリティテストも、信頼されるアプリ作りには必須です。 モバイルアプリ特有の課題である端末やOSごとの動作差分を確認する互換性テスト、そしてユーザーが迷わず目的を達成できるかを評価するユーザビリティテストも重要です。 これらのテストを網羅することで、特定の環境でしか発生しない不具合や、使い勝手の悪さに起因するユーザー離脱を未然に防ぐことができます。 リーダー候補としてプロジェクトを統括する際には、どのテストにリソースを割くべきかを論理的に判断し、効率的な検証計画を立てることが求められます。 初心者が混同しやすいテストの違い 現場で混乱を招きやすいのが、似たような名称や役割を持つテストの境界線です。 まず単体テストと結合テストの違いは、検証の範囲にあります。 単体テストはロジックの正しさを個別に確認するものですが、結合テストはそれらを繋ぎ合わせたときの「インターフェース」の不整合を見つけるものです。 また、システムテストとE2Eテストも混同されがちです。 システムテストはシステム全体が仕様通りかを検証するのに対し、E2Eテストはユーザーの最初から最後までの一連の操作(フロー)を網羅することに重点を置きます。 さらに、UIテストと受け入れテストの違いも明確にする必要があります。 UIテストは見た目や操作の挙動を確認する技術的な側面が強いですが、受け入れテストはビジネス要件を満たしているかという目的の達成に主眼を置きます。 そして、機能テストと非機能テストの違いも重要です。 機能テストは「何ができるか」を確認し、非機能テストはパフォーマンや安全性といった「どのように動作するか(品質特性)」を評価します。 これらの違いを正しく理解し、チーム内で定義を統一することは、属人化を排除し、スムーズなコミュニケーションを促進するために不可欠です。 明確な基準を設けることで、メンバー全員が同じ品質目標に向かって動けるようになり、結果としてリリース後のトラブルを大幅に削減することが可能になります。 アプリテストのやり方|実務で迷わない進め方 1. テスト対象と目的を明確にする 効率的なテストを実施するためには、まず「どの機能を何のために確認するのか」を定義することが不可欠です。 限られたリソースの中で全ての機能を網羅的に検証するのは現実的ではないため、ビジネスへの影響度が高い重要機能や、過去に不具合が頻発した障害影響の大きい領域から優先順位をつけます。 この段階でコア機能の安定性を最優先にする方針をチーム内で共有しておけば、リリースの直前になって致命的な不具合に直面するリスクを大幅に軽減できます。 また、検証項目を洗い出す際に「自動化する対象」と「しない対象」を切り分けることも重要です。 例えば、頻繁に繰り返し実行される基本機能の確認は自動化の候補となりますが、UIの微細な調整や新機能の使い勝手といった人間による感覚的な評価が必要な項目は、手動テストとして残すべきです。 このように目的を明確化し、リソースの配分を論理的に決定することで、属人化を防ぎ、チーム全体が納得感を持って作業を進められる土台が整います。 2. テスト観点を洗い出す テストの漏れを防ぎ、ユーザー視点での品質を確保するためには、多角的な観点からの洗い出しが欠かせません。 まず基本となるのは、仕様通りに正しく動くことを確認する正常系です。 しかし、実際の現場で不具合の引き金となるのは、想定外の操作を検証する異常系や、仕様の閾値となる境界値の確認不足である場合がほとんどです。 最小値や最大値、あるいはその前後の値を入力した際に予期せぬ挙動をしないか、徹底的に確認する必要があります。 さらに入力値のバリエーションやユーザー権限ごとの動作、状態遷移の整合性も重要な観点です。 特にモバイルアプリにおいては、電波の遮断といった通信エラーや、操作中の着信による中断など、スマホ特有の挙動が品質に直結します。 こうしたイレギュラーな状況をあらかじめリストアップしておくことで、現場の問題に対する不安を解消し、誰が担当しても一定の品質を保てる再現性のある検証が可能になります。 3. テストケースを作成する テストケースの作成では、一貫性と客観性が求められます。 原則として「1つのケースに対して、期待される結果は1つ」という構成を徹底し、合否判定に迷いが生じないようにします。 操作手順、入力値、そして期待される具体的な結果を明確に記述することで、経験の浅いメンバーでも正確にテストを遂行できる環境を作ります。 手順が曖昧だと再現性が失われ、後の不具合調査で混乱を招く原因になるため注意が必要です。 また検証に必要となるテストデータや実行環境は、本番環境と切り離して事前に準備しておきます。特定のユーザー状態や決済履歴が必要な場合、テストが始まってから用意していては効率が大幅に低下します。 あらかじめ必要な条件を整理し、クリーンな環境で検証を行えるように準備を整えることで、テスト工程そのものの品質が向上します。 こうした標準化されたプロセスを導入することが、将来的にプロジェクト全体を統括するリーダーとしての信頼にもつながります。 4. 実行・記録・不具合管理を行う テストの実行フェーズでは、単に結果を記録するだけでなく、不具合が発生した際の「再現条件」を詳細に残すことが極めて重要です。 どのような手順で、どのような環境下で、どのような入力を行った際に問題が起きたのかを正確に記述することで、開発エンジニアの調査コストを劇的に下げることができます。 スクリーンショットやログを併記する運用をチーム内で徹底すれば、不具合報告の精度が上がり、修正のスピード感も向上します。 修正が完了した後は、該当箇所が正しく直っているかを確認する再テストに加え、その修正が他の機能に悪影響を及ぼしていないかを確認する影響範囲の検証(回帰確認)が不可欠です。 一つの修正が別の場所で新たなバグを生む「デグレード」は、リリース後のトラブルの大きな要因となります。 記録を確実に残し、修正から確認までのプロセスを管理ツールなどで可視化することで、品質改善の進捗を客観的に把握できるようになります。 5. 回帰防止のために自動化を組み込む 継続的なリリースを行いながら品質を維持するためには、再テストの負担を軽減するための自動化が鍵となります。 特にバージョンアップのたびに繰り返し実行される基本的なシナリオは、自動テストの格好の候補です。 自動化を始める際は、期待結果がイエス・ノーで明確に定義できるものや、ビジネスインパクトが大きいコア機能から着手するのが成功のコツです。 ただし、全てのテストを自動化しようとすると、かえってメンテナンスコストが増大し、開発の足を引っ張る恐れがあります。 常に費用対効果を見極め、変更が頻繁な画面周りなどはあえて自動化を避けるといった柔軟な判断も必要です。 自動テストをCI/CD(継続的インテグレーション/継続的デリバリー)のパイプラインに組み込むことで、不具合の早期発見を仕組み化し、障害対応に追われる日々から脱却して、より価値の高い開発に時間を割ける体制を目指します。 モバイルアプリテストで必ず押さえたいチェック項目 1. インストール・起動・更新まわりのテスト アプリがユーザーの端末に無事に届き、正しく動き出すまでのプロセスは、第一印象を左右する極めて重要な工程です。 まず各プラットフォームのストアから正常にインストールできるかを確認し、ホーム画面に表示されるアイコンが指定通りのデザインで、ぼやけや欠けがないかを確認します。 起動時間については、ユーザーがストレスを感じない許容範囲内に収まっているかを実機で計測することが不可欠です。 特に現場でトラブルになりやすいのが、アプリアップデート時の挙動です。 新バージョンへ更新した際に、これまでの設定値やログイン状態、保存済みのデータが意図せず消去されることなく保持されるかを徹底して検証します。 またアンインストールを実行した後に、不要なキャッシュファイルや一時データが端末内に残ってストレージを圧迫し続けないかも確認ポイントです。 これらの項目を網羅することで、利用開始直後の離脱を防ぎ、信頼性の高いサービス提供が可能になります。 2. 画面操作・UIのテスト ユーザーが直接触れるUIの検証では、論理的な設計通りの動作と、視覚的な正確さの両面からチェックを行います。 ボタンの反応やメニュー遷移が仕様通りであることはもちろん、多種多様なアスペクト比の端末でレイアウト崩れが起きていないかを細かく見ていきます。 フォントの種類やサイズ、色のコントラスト、さらには誤字脱字といった細部まで確認を怠らないことが、プロダクト全体の質感を高める鍵となります。 モバイル特有の観点として、端末の縦横回転を切り替えた際に表示が崩れたり、入力内容がリセットされたりしないかの確認も欠かせません。 入力欄のバリデーションについては、空欄送信の防止、最大文字数制限、絵文字や記号などの特殊文字の扱い、日付や数値のフォーマット指定が適切に機能するかを一つずつ検証します。 こうした泥臭い確認の積み重ねが、リリース後の不具合報告を半減させる着実な一歩となります。 3. 通信・端末・利用環境のテスト モバイルアプリは常に安定した通信環境で使われるとは限りません。 そのため、電波が極端に弱い場所や、Wi-Fiとモバイル通信が切り替わるタイミングなど、通信が不安定な状況下でもアプリがフリーズせずに適切なエラーメッセージを表示するかを検証します。 通信エラーが発生した際のハンドリングが不十分だと、ユーザーに「壊れている」という印象を与えてしまうため、タイムアウト処理などの確認は必須です。 また、Android端末に代表される多種多様な端末差・OS差への対応も大きな課題です。 特定のメーカーやバージョンでのみ発生する表示崩れや動作不良がないか、実機やシミュレーターを駆使して互換性を確認します。 さらに、カメラや写真ライブラリへのアクセス、プッシュ通知といったデバイス固有の機能との連携がスムーズか、権限の許可・拒否設定が正しく反映されるかについても、利用環境をシミュレートしながら漏れなくチェックします。 4. 中断・再開・バックグラウンド時のテスト スマートフォンの利用シーンでは、アプリ操作中に着信やアラーム、他アプリからの通知によって操作が中断されることが日常的に起こります。 このような割り込みが発生した際でも、アプリが異常終了することなく、再開時に入力途中の内容や遷移状態が保持されているかを確認します。 バックグラウンドから復帰した瞬間に画面が真っ白になったり、データが初期化されたりする不具合は、ユーザー満足度を著しく低下させる要因です。 加えて、端末が低電力モードに入っている場合や、メモリなどのリソースが不足している低スペック端末での挙動も検証対象に含めます。 システムによってプロセスが強制終了された後の復帰プロセスが正しく設計されているかを確認することで、予期せぬクラッシュを未然に防ぎます。 こうした「動いて当たり前」の挙動を保証することが、現場のエンジニアが抱える不安を解消し、安定した運用体制を築くための土台となります。 5. 見落としやすい非機能テスト 機能が正しく動くだけでは、真に品質の高いアプリとは言えません。 性能面では、長時間利用した際のメモリリークの有無や、画面遷移のレスポンス速度といったパフォーマンスを評価します。 セキュリティ面では、重要な情報の暗号化や不必要なログの出力がないかを確認し、リスクを最小限に抑えます。 これらはリリース後に問題化すると修正コストが膨大になるため、設計段階からの意識が求められます。 また最新OSだけでなく旧バージョンとの互換性や、アプリがクラッシュせずに動き続ける安定性も重要です。 そして最後に、初めて触るユーザーでも迷わず操作できるかというユーザビリティの観点から全体を見直します。 エンジニア視点では見落としがちなこれらの非機能要件をプロセスに組み込むことで、属人化を排除した再現性のある開発体制が整います。 結果としてチーム全体の評価が高まり、テックリードやマネージャーへのキャリアアップを支える確固たる実績につながります。 アプリテストを効率化するコツと自動化の始め方 自動化に向いているテスト・向いていないテスト テスト自動化を成功させる鍵は、リソースを投入すべき対象を正しく見極めることにあります。 自動化に最も適しているのは、リリースのたびに繰り返し実行される定型的なテストや、入力値に対して期待される結果が論理的に明確なテストです。 一方で、使い心地やデザインの微細なニュアンスといった感覚的な評価が求められるUI/UXの検証は自動化には向きません。 また仕様が頻繁に変更される開発初期の機能も、スクリプトの修正コストが膨らむため、手動で柔軟に対応するほうが効率的です。 まずはコードレベルの正しさを保証する単体テストや、外部システムとの連携を確認するAPIテスト、そしてアプリの核となる主要フローの回帰テストから着手するのが定石です。 これらを自動化することで、人的ミスを防ぎながら高速に品質をチェックできる体制が整います。 現場のエンジニアが抱える「修正によるデグレード」への不安を払拭し、論理的な裏付けを持って開発を進めるための第一歩となります。 自動テスト導入の基本ステップ 自動テストを導入する際は、闇雲にコードを書くのではなく、段階的なプロセスを踏むことが重要です。 最初のステップは自動化対象の識別です。 頻度や重要度を軸に、どのテストを自動化すれば最大の効果が得られるかを判断します。 次に、検証に必要なテストデータの準備とテストケースの整理を行います。 手順や期待結果が曖昧なままでは自動化は失敗するため、誰が見ても明快な形にドキュメント化しておく必要があります。 続いて、プロジェクトの特性に合ったツールを選定し、開発フローに組み込むためのCI/CD連携を進めます。 一度構築して終わりではなく、アプリの進化に合わせてスクリプトを更新し続ける継続運用とメンテナンスの仕組み作りも欠かせません。 この一連の流れを標準化することで、属人化を排除し、チーム全体で品質を底上げできる再現性の高い開発基盤が構築されます。 ツール選定で見るべきポイント ツール選定においては、単なる機能比較だけでなく、実務での運用負荷を考慮することが不可欠です。 まずWebアプリだけでなくiOSやAndroidといったモバイル環境への対応状況を確認します。 さらに、チームの技術スタックに応じて、スクリプトを書くプログラミング型か、非エンジニアでも扱いやすいノーコード・ローコード型かを選択します。 リーダー候補としては、自分だけでなくチーム全員が使いこなせる「共有のしやすさ」も重要な評価基準となります。 また、既存のCI/CD環境との連携のしやすさや、テストコードの保守性が高いかどうかも見極めるべきポイントです。 メンテナンスに時間が取られすぎて開発が停滞しては本末転倒なため、将来的な拡張性を含めて論理的に比較検討します。 適切なツールを導入し、品質管理を仕組み化することは、プロジェクト全体を統括するマネージャーとしての視点を養う絶好の機会にもなります。 手動テストだけで終わらせない運用体制 テストを「リリース前の一時的な作業」として終わらせず、継続的な改善サイクルに組み込むことが品質向上の極意です。 不具合が発生した際には、その傾向を蓄積・分析し、テスト観点そのものをアップデートし続ける文化を醸成します。 なぜそのバグが漏れたのかを振り返り、新たな検証項目として追加することで、次回リリースでの不具合発生率を確実に下げることができます。 さらに、テストケースや知見を個人の頭の中に留めず、チームの共通資産としてドキュメント化・共有する仕組みを作ります。 これにより、特定の担当者がいないと検証が進まないといった属人化を防ぎ、常に一定の品質を保てる体制へと進化します。 トラブル対応に追われる現状を脱却し、ユーザー満足度の向上に直結する価値の高い開発に時間を投資できるよう、組織としての品質意識を高めていくことが求められます。 まとめ 高品質なアプリを安定してリリースし続けるためには、開発工程ごとに適切なテストを配置し、漏れのない検証観点を持つことが不可欠です。 単体テストから受け入れテストまでの流れを標準化し、モバイル特有の「中断・再開」や「通信環境」といったユーザー視点の項目を網羅することで、リリース後の致命的な不具合は大幅に抑制できます。 また、手動テストの柔軟性と自動テストの継続性を賢く組み合わせることは、チームの生産性を向上させるだけでなく、エンジニアがより価値の高い開発業務に集中できる環境作りにも直結します。 今回ご紹介したプロセスやチェックリストを実務に適用し、不具合の傾向を蓄積してテスト設計をアップデートし続けてみてください。 品質改善の実績は、チームからの信頼獲得や、将来的にテックリードやマネージャーとしてプロジェクトを統括するための強力な武器となるはずです。 まずは次回のリリースに向けて、優先度の高い機能のテスト設計から見直してみることをおすすめします。 QA業務効率化ならPractiTest テスト管理の効率化 についてお悩みではありませんか?そんなときはテスト資産の一元管理をすることで 工数を20%削減できる 総合テスト管理ツール「 PractiTest 」がおすすめです! PractiTest (プラクティテスト) に関する お問い合わせ トライアルアカウントお申し込みや、製品デモの依頼、 機能についての問い合わせなどお気軽にお問い合わせください。 お問い合わせ この記事の監修 Dr.T。テストエンジニア。 PractiTestエバンジェリスト。 大学卒業後、外車純正Navi開発のテストエンジニアとしてキャリアをスタート。DTVチューナ開発会社、第三者検証会社等、数々のプロダクトの検証業務に従事。 2017年株式会社モンテカンポへ入社し、マネージメント業務の傍ら、自らもテストエンジニアとしテストコンサルやPractiTestの導入サポートなどを担当している。 記事制作: 川上サトシ (マーケター、合同会社ぎあはーと代表)

動画

書籍