はじめに (対象読者・この記事でわかること)

この記事は、Androidアプリにアプリ内課金(In-App Purchase)を実装しているJava開発者を対象にしています。特に「購入が保留(Pending)になるケースをローカルで再現したい」「保留→購入完了の遷移をエミュレートしてUIテストを自動化したい」という課題を抱えている方におすすめです。
記事を読むことで、Google Play Billing Libraryで発生しうるPENDINGステータスの挙動を理解し、開発環境で手軽に再現するためのJavaコードとテスト手法が身につきます。実機や内部テックトラックを使わずに、エミュレータだけで保留→完了の一連の流れをデバッグできるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Android StudioでのJava/Kotlinプロジェクトのビルドと実行 - Google Play Billing Library 5.0以上の基本API(BillingClient, PurchasesUpdatedListener)の利用経験 - JUnit/Mockitoを使った単体テストの基礎

保留(Pending)とは何か・なぜ再現が難しいのか

アプリ内課金で「保留(Pending)」とは、ユーザーが支払いフローを開始したものの、まだ支払いが確定していない状態を指します。例えば、銀行振込やコンビニ決済、親の承認待ち(家族共有)などが該当します。
Google Play Billing LibraryはPurchase.PurchaseStatePENDING(=2)という値を定義しており、購入更新リスナーでこのステータスを受け取ると、アプリ側は「決済待ち」UIを表示する必要があります。
しかし、開発中にこのステータスを意図的に引き起こすには、実機で実際に保留になる決済手段を選ぶか、内部テックトラックでテストカードを用いる必要があり、手間と時間がかかります。本記事では、これをローカルユニットテストとエミュレータで完結させる方法を解説します。

保留ステータスをローカルで再現するJava実装とテスト手法

ここでは、Google Play Billing LibraryのBillingClientをラップしたBillingHelperクラスを作成し、保留→完了の遷移を手動でトリガーできるようにします。最後にJUnitでタイムアウトまでの状態遷移を自動テストします。

ステップ1:BillingClientをラップしたテスト用ヘルパーを作成

com.example.iapパッケージにBillingHelper.javaを作成します。本クラスはPurchasesUpdatedListenerを内部で保持し、購入状態の変更をActivityやFragmentに伝えます。テスト時はsetMockPurchaseState(int state)をコールすることで、保留状態をエミュレートできます。

Java
package com.example.iap; import android.app.Activity; import androidx.annotation.NonNull; import com.android.billingclient.api.*; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class BillingHelper implements PurchasesUpdatedListener { private final BillingClient billingClient; private final Activity activity; private CountDownLatch latch; // テスト用 private int lastPurchaseState; // テスト用 public BillingHelper(Activity activity) { this.activity = activity; this.billingClient = BillingClient.newBuilder(activity) .setListener(this) .enablePendingPurchases() // 保留購入を有効化 .build(); } public void startConnection(Runnable onConnected) { billingClient.startConnection(new BillingClientStateListener() { @Override public void onBillingSetupFinished(@NonNull BillingResult billingResult) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { onConnected.run(); } } @Override public void onBillingServiceDisconnected() { // 再接続処理(省略) } }); } /* テスト用:保留状態を手動で発行 */ public void setMockPurchaseState(int state) { Purchase mock = Purchase.newBuilder() .setProducts(List.of("test_product")) .setPurchaseState(state) .setPurchaseToken("mock_token_" + System.currentTimeMillis()) .build(); onPurchasesUpdated(BillingResult.newBuilder() .setResponseCode(BillingClient.BillingResponseCode.OK) .build(), List.of(mock)); } @Override public void onPurchasesUpdated(@NonNull BillingResult billingResult, List<Purchase> purchases) { if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) { for (Purchase p : purchases) { lastPurchaseState = p.getPurchaseState(); handlePurchase(p); } } if (latch != null) { latch.countDown(); // テスト待機を解除 } } private void handlePurchase(Purchase purchase) { if (purchase.getPurchaseState() == Purchase.PurchaseState.PENDING) { // 保留UIを表示 Toast.makeText(activity, "決済保留中です", Toast.LENGTH_LONG).show(); } else if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) { // 購入完了 Toast.makeText(activity, "購入完了しました", Toast.LENGTH_LONG).show(); // 消耗品の場合はconsume ConsumeParams consumeParams = ConsumeParams.newBuilder() .setPurchaseToken(purchase.getPurchaseToken()) .build(); billingClient.consumeAsync(consumeParams, (result, s) -> {}); } } /* テスト用:状態をポーリング */ public boolean waitForStateChange(long timeout, TimeUnit unit) throws InterruptedException { latch = new CountDownLatch(1); return latch.await(timeout, unit); } public int getLastPurchaseState() { return lastPurchaseState; } }

ステップ2:エミュレータで保留→完了を手動で遷移させる

  1. Android Studioでエミュレータを起動し、Google Playがインストールされたイメージ(Play Store対応)を選びます。
  2. アプリをインストール後、以下のように保留を再現します。
Java
BillingHelper helper = new BillingHelper(MainActivity.this); helper.startConnection(() -> { // 保留状態を発行 helper.setMockPurchaseState(Purchase.PurchaseState.PENDING); // 5秒後に完了にする new Handler().postDelayed(() -> helper.setMockPurchaseState(Purchase.PurchaseState.PURCHASED), 5000); });

このコードにより、保留トースト→5秒後に完了トーストが表示され、実際の決済フローと同じUI遷移が確認できます。

ハマった点:保留→完了の遷移がlistenerに届かない

setMockPurchaseStatePurchaseState.PURCHASEDに変更しても、listenerが再び呼ばれないケースがありました。これは、BillingClientが同一トークンで重複した購入情報をフィルタリングしてしまうためです。
回避策として、トークンにタイムスタンプを含めることで常に一意の購入として扱わせています("mock_token_" + System.currentTimeMillis())。

解決策:トークンにタイムスタンプを含める

上記の通り、トークンをユニークにすることで、何度でもonPurchasesUpdatedが呼ばれるようになり、保留→完了を自由にエミュレートできるようになりました。

まとめ

本記事では、Google Play Billing Libraryで発生する保留(Pending)ステータスを、Javaで簡単にローカル再現する方法を解説しました。

  • BillingClientをラップしたBillingHelperを作成し、保留状態を手動で発行するsetMockPurchaseStateを実装
  • エミュレータで保留→完了の遷移を手軽にテスト
  • トークンをユニークにすることで、繰り返しlistenerを呼べるようにする

この手法を使えば、実機や内部テックトラックを使わずとも、保留決済のUIとタイムアウト/完了フローを高速に検証できます。
次回は、保留決済のタイムアウト(48時間経過でキャンセル)を疑似再現し、自動テストに組み込む方法を紹介する予定です。

参考資料